@telecrypt-io/storage 0.5.28 → 0.5.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +96 -157
- 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,10 +197,12 @@ async function resolveFile(storage, tree, fileId, signal) {
|
|
|
197
197
|
try {
|
|
198
198
|
return await waitForCondition(() => (isMarkedFileDeleted(storage, tree.id, fileId) ? null : tree.getFile(fileId)), { timeoutMs: 15000, signal });
|
|
199
199
|
}
|
|
200
|
-
catch {
|
|
200
|
+
catch (error) {
|
|
201
201
|
if (signal?.aborted)
|
|
202
202
|
throw new StorageError("operation cancelled");
|
|
203
|
-
|
|
203
|
+
if (error instanceof ConditionTimeoutError)
|
|
204
|
+
throw new StorageError("file not found");
|
|
205
|
+
throw new StorageError("file lookup failed", { cause: error });
|
|
204
206
|
}
|
|
205
207
|
}
|
|
206
208
|
function snapshotTreeSpaces(root, tooLargeMessage = "storage tree is too large") {
|
|
@@ -223,8 +225,8 @@ function snapshotTreeSpaces(root, tooLargeMessage = "storage tree is too large")
|
|
|
223
225
|
try {
|
|
224
226
|
children = tree.getDirectories();
|
|
225
227
|
}
|
|
226
|
-
catch {
|
|
227
|
-
throw new StorageError("could not enumerate storage space descendants safely");
|
|
228
|
+
catch (error) {
|
|
229
|
+
throw new StorageError("could not enumerate storage space descendants safely", { cause: error });
|
|
228
230
|
}
|
|
229
231
|
if (children.length > MAX_DELETION_ROOMS) {
|
|
230
232
|
throw new StorageError(tooLargeMessage);
|
|
@@ -250,8 +252,8 @@ function assertTreeEmptyForDeletion(storage, tree, spaces) {
|
|
|
250
252
|
.filter((file) => !isMarkedFileDeleted(storage, tree.id, file.id) &&
|
|
251
253
|
!isConfirmedDeletedFile(storage.getClient(), tree.id, file));
|
|
252
254
|
}
|
|
253
|
-
catch {
|
|
254
|
-
throw new StorageError("could not enumerate storage files safely");
|
|
255
|
+
catch (error) {
|
|
256
|
+
throw new StorageError("could not enumerate storage files safely", { cause: error });
|
|
255
257
|
}
|
|
256
258
|
if (files.length > 0)
|
|
257
259
|
throw new NonEmptyTreeError(tree.id);
|
|
@@ -360,8 +362,8 @@ function validateDeletionGraph(client, root, ids, deletedRooms) {
|
|
|
360
362
|
try {
|
|
361
363
|
directories = tree.getDirectories();
|
|
362
364
|
}
|
|
363
|
-
catch {
|
|
364
|
-
throw new StorageError("delete graph is unsafe");
|
|
365
|
+
catch (error) {
|
|
366
|
+
throw new StorageError("delete graph is unsafe", { cause: error });
|
|
365
367
|
}
|
|
366
368
|
for (const child of directories)
|
|
367
369
|
addEdge(id, child.id);
|
|
@@ -455,8 +457,10 @@ async function unlinkExternalParents(storage, rootId, externalParents, signal) {
|
|
|
455
457
|
try {
|
|
456
458
|
await relinkExternalParents(storage, rootId, externalParents, new AbortController().signal);
|
|
457
459
|
}
|
|
458
|
-
catch {
|
|
459
|
-
throw new StorageError("delete graph unlink cleanup is incomplete"
|
|
460
|
+
catch (cleanupError) {
|
|
461
|
+
throw new StorageError("delete graph unlink cleanup is incomplete", {
|
|
462
|
+
cause: new AggregateError([error, cleanupError], "delete graph unlink and relink both failed", { cause: error }),
|
|
463
|
+
});
|
|
460
464
|
}
|
|
461
465
|
throw error;
|
|
462
466
|
}
|
|
@@ -554,7 +558,7 @@ async function deleteRoomDeterministically(storage, roomId, signal) {
|
|
|
554
558
|
if (error instanceof MutationOutcomeUnknownError || error instanceof MutationPartialError)
|
|
555
559
|
throw error;
|
|
556
560
|
if (completedRoomIds.length > 0) {
|
|
557
|
-
throw new MutationPartialError("delete", completedRoomIds, "room cleanup stopped");
|
|
561
|
+
throw new MutationPartialError("delete", completedRoomIds, "room cleanup stopped", { cause: error });
|
|
558
562
|
}
|
|
559
563
|
throw error;
|
|
560
564
|
}
|
|
@@ -565,14 +569,7 @@ async function deleteRoomDeterministically(storage, roomId, signal) {
|
|
|
565
569
|
* may otherwise leave a stale invite visible until the next sync.
|
|
566
570
|
*/
|
|
567
571
|
function removeRoomFromLocalStore(client, roomId) {
|
|
568
|
-
|
|
569
|
-
client.store?.removeRoom(roomId);
|
|
570
|
-
}
|
|
571
|
-
catch {
|
|
572
|
-
// Server-side leave/forget already completed; a store implementation that
|
|
573
|
-
// cannot evict synchronously must not turn authoritative cleanup into a
|
|
574
|
-
// false failure.
|
|
575
|
-
}
|
|
572
|
+
client.store?.removeRoom(roomId);
|
|
576
573
|
}
|
|
577
574
|
function isActiveMembership(membership) {
|
|
578
575
|
return membership === "join" || membership === "invite";
|
|
@@ -582,12 +579,11 @@ function isRevocableMembership(membership) {
|
|
|
582
579
|
}
|
|
583
580
|
function safePartialDetail(error) {
|
|
584
581
|
// Provider/Matrix errors can contain response bodies, URLs, or credentials.
|
|
585
|
-
// StorageError messages are authored by this package
|
|
586
|
-
//
|
|
587
|
-
// owner/self-target diagnostics.
|
|
582
|
+
// StorageError messages are authored by this package; retain those local
|
|
583
|
+
// policy details so callers still get useful owner/self-target diagnostics.
|
|
588
584
|
if (!(error instanceof StorageError) || error instanceof MutationOutcomeUnknownError)
|
|
589
585
|
return undefined;
|
|
590
|
-
return error.message
|
|
586
|
+
return error.message;
|
|
591
587
|
}
|
|
592
588
|
function toArrayBuffer(bytes) {
|
|
593
589
|
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
@@ -599,16 +595,12 @@ export async function createVault(storage, name, options) {
|
|
|
599
595
|
tree = await withRateLimitRetry(() => storage.createTree(name, signal), signal);
|
|
600
596
|
}
|
|
601
597
|
catch (error) {
|
|
602
|
-
if (error
|
|
603
|
-
error instanceof RoomCreationAmbiguousError ||
|
|
604
|
-
error instanceof MutationOutcomeUnknownError ||
|
|
605
|
-
(error instanceof Error &&
|
|
606
|
-
error.cleanupIncomplete === true)) {
|
|
598
|
+
if (requiresMutationReconciliation(error)) {
|
|
607
599
|
throw error;
|
|
608
600
|
}
|
|
609
601
|
if (signal.aborted)
|
|
610
602
|
throw new StorageError("operation cancelled");
|
|
611
|
-
throw new StorageError("create vault failed");
|
|
603
|
+
throw new StorageError("create vault failed", { cause: error });
|
|
612
604
|
}
|
|
613
605
|
ensureOperationActive(signal);
|
|
614
606
|
return { id: tree.id, name };
|
|
@@ -646,7 +638,7 @@ export async function joinVault(storage, vaultId, options) {
|
|
|
646
638
|
membership = null;
|
|
647
639
|
}
|
|
648
640
|
else {
|
|
649
|
-
throw new StorageError("join failed");
|
|
641
|
+
throw new StorageError("join failed", { cause: err });
|
|
650
642
|
}
|
|
651
643
|
}
|
|
652
644
|
if (membership === "join")
|
|
@@ -664,7 +656,7 @@ export async function joinVault(storage, vaultId, options) {
|
|
|
664
656
|
}
|
|
665
657
|
if (signal.aborted)
|
|
666
658
|
throw new StorageError("operation cancelled");
|
|
667
|
-
throw new StorageError("join failed");
|
|
659
|
+
throw new StorageError("join failed", { cause: err });
|
|
668
660
|
}
|
|
669
661
|
ensureOperationActive(signal);
|
|
670
662
|
return { vaultId, joined: true };
|
|
@@ -700,8 +692,6 @@ export async function listPendingInvites(storage, options) {
|
|
|
700
692
|
return withOperationDeadline(options, async (signal) => {
|
|
701
693
|
const client = storage.getClient();
|
|
702
694
|
const rooms = client.getRooms();
|
|
703
|
-
if (rooms.length > MAX_LIST_ITEMS)
|
|
704
|
-
throw new StorageError("invite list is too large");
|
|
705
695
|
const invites = [];
|
|
706
696
|
for (const room of rooms) {
|
|
707
697
|
ensureOperationActive(signal);
|
|
@@ -820,7 +810,7 @@ export async function declineInvite(storage, vaultId, options) {
|
|
|
820
810
|
throw error;
|
|
821
811
|
if (signal.aborted)
|
|
822
812
|
throw new StorageError("operation cancelled");
|
|
823
|
-
throw new StorageError("decline failed");
|
|
813
|
+
throw new StorageError("decline failed", { cause: error });
|
|
824
814
|
}
|
|
825
815
|
ensureOperationActive(signal);
|
|
826
816
|
return { vaultId, declined: true };
|
|
@@ -893,7 +883,7 @@ export async function shareVault(storage, vaultId, userId, role, options) {
|
|
|
893
883
|
if (error instanceof MutationOutcomeUnknownError)
|
|
894
884
|
throw error;
|
|
895
885
|
if (completedRoomIds.size > 0) {
|
|
896
|
-
throw new MutationPartialError("share", [...completedRoomIds], safePartialDetail(error));
|
|
886
|
+
throw new MutationPartialError("share", [...completedRoomIds], safePartialDetail(error), { cause: error });
|
|
897
887
|
}
|
|
898
888
|
if (error instanceof StorageError &&
|
|
899
889
|
(error.message === "share failed" ||
|
|
@@ -902,7 +892,7 @@ export async function shareVault(storage, vaultId, userId, role, options) {
|
|
|
902
892
|
error.message === "operation cancelled")) {
|
|
903
893
|
throw error;
|
|
904
894
|
}
|
|
905
|
-
throw new StorageError("share failed");
|
|
895
|
+
throw new StorageError("share failed", { cause: error });
|
|
906
896
|
}
|
|
907
897
|
return { vaultId, userId, role };
|
|
908
898
|
}, operation.signal);
|
|
@@ -968,7 +958,7 @@ export async function unshareVault(storage, vaultId, userId, options) {
|
|
|
968
958
|
if (error instanceof MutationOutcomeUnknownError)
|
|
969
959
|
throw error;
|
|
970
960
|
if (completedRoomIds.size > 0) {
|
|
971
|
-
throw new MutationPartialError("unshare", [...completedRoomIds], safePartialDetail(error));
|
|
961
|
+
throw new MutationPartialError("unshare", [...completedRoomIds], safePartialDetail(error), { cause: error });
|
|
972
962
|
}
|
|
973
963
|
if (error instanceof StorageError &&
|
|
974
964
|
(error.message.includes("owner") ||
|
|
@@ -976,7 +966,7 @@ export async function unshareVault(storage, vaultId, userId, options) {
|
|
|
976
966
|
error.message === "operation cancelled")) {
|
|
977
967
|
throw error;
|
|
978
968
|
}
|
|
979
|
-
throw new StorageError("unshare failed");
|
|
969
|
+
throw new StorageError("unshare failed", { cause: error });
|
|
980
970
|
}
|
|
981
971
|
return { vaultId, userId, removed: true };
|
|
982
972
|
}, operation.signal);
|
|
@@ -992,10 +982,10 @@ export async function listMembers(storage, vaultId, options) {
|
|
|
992
982
|
try {
|
|
993
983
|
return await withRateLimitRetry(() => storage.listMembers(tree, { signal }), signal);
|
|
994
984
|
}
|
|
995
|
-
catch {
|
|
985
|
+
catch (error) {
|
|
996
986
|
if (signal.aborted)
|
|
997
987
|
throw new StorageError("operation cancelled");
|
|
998
|
-
throw new StorageError("list members failed");
|
|
988
|
+
throw new StorageError("list members failed", { cause: error });
|
|
999
989
|
}
|
|
1000
990
|
});
|
|
1001
991
|
}
|
|
@@ -1008,16 +998,12 @@ export async function listFiles(storage, treeId, options) {
|
|
|
1008
998
|
const files = tree
|
|
1009
999
|
.listFiles()
|
|
1010
1000
|
.filter((file) => !isMarkedFileDeleted(storage, tree.id, file.id));
|
|
1011
|
-
if (files.length > MAX_LIST_ITEMS)
|
|
1012
|
-
throw new StorageError("file list is too large");
|
|
1013
1001
|
return files.map((f) => ({ id: f.id, name: f.getName() }));
|
|
1014
1002
|
}
|
|
1015
1003
|
catch (error) {
|
|
1016
1004
|
if (signal.aborted)
|
|
1017
1005
|
throw new StorageError("operation cancelled");
|
|
1018
|
-
|
|
1019
|
-
throw error;
|
|
1020
|
-
throw new StorageError("list files failed");
|
|
1006
|
+
throw new StorageError("list files failed", { cause: error });
|
|
1021
1007
|
}
|
|
1022
1008
|
});
|
|
1023
1009
|
}
|
|
@@ -1030,8 +1016,6 @@ export async function listSubfolders(storage, parentId, options) {
|
|
|
1030
1016
|
const directories = tree
|
|
1031
1017
|
.getDirectories()
|
|
1032
1018
|
.filter((directory) => !isMarkedTreeDeleted(storage, directory.id));
|
|
1033
|
-
if (directories.length > MAX_LIST_ITEMS)
|
|
1034
|
-
throw new StorageError("folder list is too large");
|
|
1035
1019
|
return directories.map((d) => ({ id: d.id, name: d.room.name }));
|
|
1036
1020
|
});
|
|
1037
1021
|
}
|
|
@@ -1044,16 +1028,12 @@ export async function createSubfolder(storage, parentId, name, options) {
|
|
|
1044
1028
|
sub = await withRateLimitRetry(() => storage.createSubtree(tree, name, signal), signal);
|
|
1045
1029
|
}
|
|
1046
1030
|
catch (error) {
|
|
1047
|
-
if (error
|
|
1048
|
-
error instanceof RoomCreationAmbiguousError ||
|
|
1049
|
-
error instanceof MutationOutcomeUnknownError ||
|
|
1050
|
-
(error instanceof Error &&
|
|
1051
|
-
error.cleanupIncomplete === true)) {
|
|
1031
|
+
if (requiresMutationReconciliation(error)) {
|
|
1052
1032
|
throw error;
|
|
1053
1033
|
}
|
|
1054
1034
|
if (signal.aborted)
|
|
1055
1035
|
throw new StorageError("operation cancelled");
|
|
1056
|
-
throw new StorageError("create folder failed");
|
|
1036
|
+
throw new StorageError("create folder failed", { cause: error });
|
|
1057
1037
|
}
|
|
1058
1038
|
ensureOperationActive(signal);
|
|
1059
1039
|
return { id: sub.id, name };
|
|
@@ -1071,12 +1051,7 @@ async function renameTree(storage, treeId, name, options) {
|
|
|
1071
1051
|
const current = storage.getTree(treeId);
|
|
1072
1052
|
if (current?.room.name === name)
|
|
1073
1053
|
return current;
|
|
1074
|
-
|
|
1075
|
-
await storage.refreshRoomState(treeId, { signal });
|
|
1076
|
-
}
|
|
1077
|
-
catch {
|
|
1078
|
-
return null;
|
|
1079
|
-
}
|
|
1054
|
+
await storage.refreshRoomState(treeId, { signal });
|
|
1080
1055
|
const refreshed = storage.getTree(treeId);
|
|
1081
1056
|
return refreshed?.room.name === name ? refreshed : null;
|
|
1082
1057
|
}, { timeoutMs: 15000, signal });
|
|
@@ -1086,7 +1061,7 @@ async function renameTree(storage, treeId, name, options) {
|
|
|
1086
1061
|
throw error;
|
|
1087
1062
|
if (signal.aborted)
|
|
1088
1063
|
throw new StorageError("operation cancelled");
|
|
1089
|
-
throw new StorageError("rename failed");
|
|
1064
|
+
throw new StorageError("rename failed", { cause: error });
|
|
1090
1065
|
}
|
|
1091
1066
|
return { id: treeId, name };
|
|
1092
1067
|
}, "mutation");
|
|
@@ -1132,13 +1107,9 @@ async function deleteTree(storage, treeId, options) {
|
|
|
1132
1107
|
graph = validateDeletionGraph(client, tree, ids, removedRooms);
|
|
1133
1108
|
}
|
|
1134
1109
|
catch (error) {
|
|
1135
|
-
if (error instanceof MutationOutcomeUnknownError ||
|
|
1136
|
-
error instanceof MutationPartialError ||
|
|
1137
|
-
error instanceof NonEmptyTreeError)
|
|
1138
|
-
throw error;
|
|
1139
1110
|
if (error instanceof StorageError)
|
|
1140
1111
|
throw error;
|
|
1141
|
-
throw new StorageError("delete failed");
|
|
1112
|
+
throw new StorageError("delete failed", { cause: error });
|
|
1142
1113
|
}
|
|
1143
1114
|
// Keep external parent links intact until the room deletion succeeds.
|
|
1144
1115
|
// Unlinking first would silently detach a live vault on failure.
|
|
@@ -1146,16 +1117,9 @@ async function deleteTree(storage, treeId, options) {
|
|
|
1146
1117
|
await unlinkExternalParents(storage, tree.id, graph.externalParents, operation.signal);
|
|
1147
1118
|
}
|
|
1148
1119
|
catch (error) {
|
|
1149
|
-
if (error instanceof MutationOutcomeUnknownError)
|
|
1150
|
-
throw error;
|
|
1151
|
-
if (error instanceof MutationPartialError)
|
|
1152
|
-
throw error;
|
|
1153
|
-
if (error instanceof StorageError && error.message === "delete graph unlink cleanup is incomplete") {
|
|
1154
|
-
throw error;
|
|
1155
|
-
}
|
|
1156
1120
|
if (error instanceof StorageError)
|
|
1157
1121
|
throw error;
|
|
1158
|
-
throw new StorageError("delete failed");
|
|
1122
|
+
throw new StorageError("delete failed", { cause: error });
|
|
1159
1123
|
}
|
|
1160
1124
|
try {
|
|
1161
1125
|
await deleteRoomDeterministically(storage, tree.id, operation.signal);
|
|
@@ -1167,16 +1131,14 @@ async function deleteTree(storage, treeId, options) {
|
|
|
1167
1131
|
try {
|
|
1168
1132
|
await relinkExternalParents(storage, tree.id, graph.externalParents, new AbortController().signal);
|
|
1169
1133
|
}
|
|
1170
|
-
catch {
|
|
1171
|
-
throw new StorageError("delete cleanup is incomplete"
|
|
1134
|
+
catch (cleanupError) {
|
|
1135
|
+
throw new StorageError("delete cleanup is incomplete", {
|
|
1136
|
+
cause: new AggregateError([error, cleanupError], "delete and external-parent relink both failed", { cause: error }),
|
|
1137
|
+
});
|
|
1172
1138
|
}
|
|
1173
|
-
if (error instanceof MutationOutcomeUnknownError)
|
|
1174
|
-
throw error;
|
|
1175
|
-
if (error instanceof MutationPartialError)
|
|
1176
|
-
throw error;
|
|
1177
1139
|
if (error instanceof StorageError)
|
|
1178
1140
|
throw error;
|
|
1179
|
-
throw new StorageError("delete failed");
|
|
1141
|
+
throw new StorageError("delete failed", { cause: error });
|
|
1180
1142
|
}
|
|
1181
1143
|
return { id: treeId, deleted: true };
|
|
1182
1144
|
}, operation.signal);
|
|
@@ -1216,7 +1178,7 @@ export async function renameFile(storage, treeId, fileId, name, options) {
|
|
|
1216
1178
|
throw error;
|
|
1217
1179
|
if (signal.aborted)
|
|
1218
1180
|
throw new StorageError("operation cancelled");
|
|
1219
|
-
throw new StorageError("rename file failed");
|
|
1181
|
+
throw new StorageError("rename file failed", { cause: error });
|
|
1220
1182
|
}
|
|
1221
1183
|
return { id: fileId, name };
|
|
1222
1184
|
}, "mutation");
|
|
@@ -1235,11 +1197,11 @@ async function resolveFileVersions(branch, signal) {
|
|
|
1235
1197
|
try {
|
|
1236
1198
|
history = await branch.getVersionHistory();
|
|
1237
1199
|
}
|
|
1238
|
-
catch {
|
|
1239
|
-
throw new StorageError("could not resolve file version history safely");
|
|
1200
|
+
catch (error) {
|
|
1201
|
+
throw new StorageError("could not resolve file version history safely", { cause: error });
|
|
1240
1202
|
}
|
|
1241
|
-
if (!Array.isArray(history) || history.length === 0
|
|
1242
|
-
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");
|
|
1243
1205
|
}
|
|
1244
1206
|
const eventIds = new Set();
|
|
1245
1207
|
const mediaIds = new Set();
|
|
@@ -1253,8 +1215,8 @@ async function resolveFileVersions(branch, signal) {
|
|
|
1253
1215
|
try {
|
|
1254
1216
|
validateMatrixEventId(version.id, "file version event ID");
|
|
1255
1217
|
}
|
|
1256
|
-
catch {
|
|
1257
|
-
throw new StorageError("file version history contains an invalid event ID");
|
|
1218
|
+
catch (error) {
|
|
1219
|
+
throw new StorageError("file version history contains an invalid event ID", { cause: error });
|
|
1258
1220
|
}
|
|
1259
1221
|
if (eventIds.has(version.id)) {
|
|
1260
1222
|
throw new StorageError("file version history contains a cycle");
|
|
@@ -1264,17 +1226,14 @@ async function resolveFileVersions(branch, signal) {
|
|
|
1264
1226
|
try {
|
|
1265
1227
|
fileInfo = await version.getFileInfo();
|
|
1266
1228
|
}
|
|
1267
|
-
catch {
|
|
1268
|
-
throw new StorageError("could not resolve encrypted file version safely");
|
|
1229
|
+
catch (error) {
|
|
1230
|
+
throw new StorageError("could not resolve encrypted file version safely", { cause: error });
|
|
1269
1231
|
}
|
|
1270
1232
|
const mediaId = fileInfo?.info?.url;
|
|
1271
1233
|
if (typeof mediaId !== "string" || mediaId.length === 0) {
|
|
1272
1234
|
throw new StorageError("encrypted file version has no media identifier");
|
|
1273
1235
|
}
|
|
1274
1236
|
mediaIds.add(mediaId);
|
|
1275
|
-
if (mediaIds.size > MAX_FILE_VERSION_CHAIN) {
|
|
1276
|
-
throw new StorageError("file media version history is too large");
|
|
1277
|
-
}
|
|
1278
1237
|
versions.push({ branch: version, mediaId });
|
|
1279
1238
|
}
|
|
1280
1239
|
return versions;
|
|
@@ -1297,7 +1256,7 @@ async function deleteFileMedia(storage, mediaIds, signal) {
|
|
|
1297
1256
|
throw error;
|
|
1298
1257
|
if (signal?.aborted)
|
|
1299
1258
|
throw new StorageError("operation cancelled");
|
|
1300
|
-
throw new StorageError("delete file media failed");
|
|
1259
|
+
throw new StorageError("delete file media failed", { cause: error });
|
|
1301
1260
|
}
|
|
1302
1261
|
}
|
|
1303
1262
|
export async function deleteFile(storage, treeId, fileId, options) {
|
|
@@ -1322,9 +1281,9 @@ export async function deleteFile(storage, treeId, fileId, options) {
|
|
|
1322
1281
|
if (error instanceof MutationOutcomeUnknownError)
|
|
1323
1282
|
throw error;
|
|
1324
1283
|
if (signal.aborted) {
|
|
1325
|
-
throw new MutationPartialError("delete file", completedIds, "media was deleted but Matrix event cleanup was cancelled");
|
|
1284
|
+
throw new MutationPartialError("delete file", completedIds, "media was deleted but Matrix event cleanup was cancelled", { cause: error });
|
|
1326
1285
|
}
|
|
1327
|
-
throw new MutationPartialError("delete file", completedIds, "media was deleted but Matrix event cleanup stopped");
|
|
1286
|
+
throw new MutationPartialError("delete file", completedIds, "media was deleted but Matrix event cleanup stopped", { cause: error });
|
|
1328
1287
|
}
|
|
1329
1288
|
try {
|
|
1330
1289
|
await waitForCondition(async () => {
|
|
@@ -1338,7 +1297,7 @@ export async function deleteFile(storage, treeId, fileId, options) {
|
|
|
1338
1297
|
if (error instanceof MutationOutcomeUnknownError)
|
|
1339
1298
|
throw error;
|
|
1340
1299
|
const detail = "Matrix deletion completed but the inactive file state could not be verified";
|
|
1341
|
-
throw new MutationPartialError("delete file", completedIds, detail);
|
|
1300
|
+
throw new MutationPartialError("delete file", completedIds, detail, { cause: error });
|
|
1342
1301
|
}
|
|
1343
1302
|
for (const { branch: version } of versions) {
|
|
1344
1303
|
markFileDeleted(storage.getClient(), tree.id, version.id);
|
|
@@ -1357,7 +1316,7 @@ export async function uploadFile(storage, treeId, name, bytes, mimetype, options
|
|
|
1357
1316
|
throw error;
|
|
1358
1317
|
if (signal.aborted)
|
|
1359
1318
|
throw new StorageError("operation cancelled");
|
|
1360
|
-
throw new StorageError("upload failed");
|
|
1319
|
+
throw new StorageError("upload failed", { cause: error });
|
|
1361
1320
|
});
|
|
1362
1321
|
// The create-file request is acknowledged before the event necessarily
|
|
1363
1322
|
// arrives in this client's sync timeline. A caller can otherwise report a
|
|
@@ -1376,7 +1335,7 @@ export async function uploadFile(storage, treeId, name, bytes, mimetype, options
|
|
|
1376
1335
|
catch (error) {
|
|
1377
1336
|
if (error instanceof MutationOutcomeUnknownError)
|
|
1378
1337
|
throw error;
|
|
1379
|
-
throw new MutationPartialError("upload file", [fileId], "upload completed but the file could not be observed locally");
|
|
1338
|
+
throw new MutationPartialError("upload file", [fileId], "upload completed but the file could not be observed locally", { cause: error });
|
|
1380
1339
|
}
|
|
1381
1340
|
ensureOperationActive(signal);
|
|
1382
1341
|
return { id: fileId, name, mimetype };
|
|
@@ -1391,11 +1350,11 @@ export async function downloadFile(storage, treeId, fileId, options) {
|
|
|
1391
1350
|
result = await storage.downloadFile(branch, signal);
|
|
1392
1351
|
}
|
|
1393
1352
|
catch (error) {
|
|
1394
|
-
if (error instanceof
|
|
1353
|
+
if (error instanceof StorageError)
|
|
1395
1354
|
throw error;
|
|
1396
1355
|
if (signal.aborted)
|
|
1397
1356
|
throw new StorageError("operation cancelled");
|
|
1398
|
-
throw new StorageError("download failed");
|
|
1357
|
+
throw new StorageError("download failed", { cause: error });
|
|
1399
1358
|
}
|
|
1400
1359
|
ensureOperationActive(signal);
|
|
1401
1360
|
return {
|
|
@@ -1425,41 +1384,21 @@ export async function getFileDetails(storage, treeId, fileId, options) {
|
|
|
1425
1384
|
let size = null;
|
|
1426
1385
|
let createdAt = null;
|
|
1427
1386
|
let updatedAt = null;
|
|
1428
|
-
try {
|
|
1429
|
-
ensureOperationActive(signal);
|
|
1430
|
-
const { info } = await branch.getFileInfo();
|
|
1431
|
-
if (info) {
|
|
1432
|
-
if (typeof info["mimetype"] === "string")
|
|
1433
|
-
mimetype = info["mimetype"];
|
|
1434
|
-
if (typeof info["size"] === "number")
|
|
1435
|
-
size = info["size"];
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
catch {
|
|
1439
|
-
if (signal.aborted)
|
|
1440
|
-
throw new StorageError("operation cancelled");
|
|
1441
|
-
// Partial metadata is fine — UI shows "—" for unknown fields.
|
|
1442
|
-
}
|
|
1443
1387
|
try {
|
|
1444
1388
|
ensureOperationActive(signal);
|
|
1445
1389
|
const event = await branch.getFileEvent();
|
|
1446
|
-
const
|
|
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);
|
|
1390
|
+
const metadata = readFileEventMetadata(event.getContent());
|
|
1391
|
+
mimetype = metadata.mimetype;
|
|
1392
|
+
size = metadata.size;
|
|
1393
|
+
createdAt = tsToIso(event.getTs());
|
|
1457
1394
|
updatedAt = createdAt;
|
|
1458
1395
|
}
|
|
1459
|
-
catch {
|
|
1396
|
+
catch (error) {
|
|
1460
1397
|
if (signal.aborted)
|
|
1461
1398
|
throw new StorageError("operation cancelled");
|
|
1462
|
-
|
|
1399
|
+
if (error instanceof StorageError)
|
|
1400
|
+
throw error;
|
|
1401
|
+
throw new StorageError("get file details failed", { cause: error });
|
|
1463
1402
|
}
|
|
1464
1403
|
ensureOperationActive(signal);
|
|
1465
1404
|
return { name, mimetype, size, createdAt, updatedAt };
|
|
@@ -1479,8 +1418,8 @@ async function getTreeDetails(storage, treeId, signal) {
|
|
|
1479
1418
|
try {
|
|
1480
1419
|
memberCount = room.getJoinedMemberCount();
|
|
1481
1420
|
}
|
|
1482
|
-
catch {
|
|
1483
|
-
|
|
1421
|
+
catch (error) {
|
|
1422
|
+
throw new StorageError("member count lookup failed", { cause: error });
|
|
1484
1423
|
}
|
|
1485
1424
|
}
|
|
1486
1425
|
return {
|
package/dist/core/poll.d.ts
CHANGED
package/dist/core/poll.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export class ConditionTimeoutError extends Error {
|
|
2
|
+
constructor(timeoutMs) {
|
|
3
|
+
super(`timed out after ${timeoutMs}ms waiting for condition`);
|
|
4
|
+
this.name = "ConditionTimeoutError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
1
7
|
/**
|
|
2
8
|
* A freshly-synced client doesn't always see a room/event the *instant* the
|
|
3
9
|
* server has committed it — e.g. a room this same session just created can
|
|
@@ -12,7 +18,7 @@ export async function waitForCondition(check, opts) {
|
|
|
12
18
|
const timeoutMs = opts?.timeoutMs ?? 15000;
|
|
13
19
|
const intervalMs = opts?.intervalMs ?? 300;
|
|
14
20
|
const deadline = Date.now() + timeoutMs;
|
|
15
|
-
const timeoutError = () => new
|
|
21
|
+
const timeoutError = () => new ConditionTimeoutError(timeoutMs);
|
|
16
22
|
const abortError = () => {
|
|
17
23
|
const reason = opts?.signal?.reason;
|
|
18
24
|
return reason instanceof Error ? reason : new Error("condition wait aborted");
|