@telecrypt-io/storage 0.2.3 → 0.2.4
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 +3 -3
- package/dist/core/operations.js +58 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,14 +8,14 @@ Files are encrypted on the client before upload. The server stores only opaque c
|
|
|
8
8
|
never holds the decryption keys. Shared folders let multiple people add and read files, and a
|
|
9
9
|
Recovery Key restores your files on a new device — even if you lose the original.
|
|
10
10
|
|
|
11
|
-
**Status:** library source only. `@telecrypt-io/storage@0.2.
|
|
12
|
-
published from this repository's protected `v0.2.
|
|
11
|
+
**Status:** library source only. `@telecrypt-io/storage@0.2.4` is the current public release,
|
|
12
|
+
published from this repository's protected `v0.2.4` tag with NPM Trusted Publishing/OIDC
|
|
13
13
|
provenance. The older `0.1.3` release is the immutable legacy combined library-and-CLI package.
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
npm install @telecrypt-io/storage@0.2.
|
|
18
|
+
npm install @telecrypt-io/storage@0.2.4
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
This gives you the `TeleCryptIOStorage` library and its browser-safe `core` API.
|
package/dist/core/operations.js
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
import { CliError } from "./errors.js";
|
|
2
2
|
import { waitForCondition } from "./poll.js";
|
|
3
|
+
/**
|
|
4
|
+
* Production Synapse enforces the built-in rc_messages budget (per-account
|
|
5
|
+
* burst 10, refill 1/5s) on every room/state mutation the tree operations
|
|
6
|
+
* issue. A delete/decline sequence (kick members + leave + forget) can
|
|
7
|
+
* exceed the burst within seconds, so each operation retries on 429 with the
|
|
8
|
+
* server-advised backoff. Mirrors the harness library suite's
|
|
9
|
+
* `withRateLimitRetry`; must not mask non-rate-limit failures.
|
|
10
|
+
*/
|
|
11
|
+
const RATE_LIMIT_RETRIES = 6;
|
|
12
|
+
function isRateLimited(error) {
|
|
13
|
+
if (!(error instanceof Error))
|
|
14
|
+
return false;
|
|
15
|
+
if (!("isRateLimitError" in error))
|
|
16
|
+
return false;
|
|
17
|
+
const isRateLimitError = error.isRateLimitError;
|
|
18
|
+
return typeof isRateLimitError === "function" && isRateLimitError.call(error) === true;
|
|
19
|
+
}
|
|
20
|
+
async function withRateLimitRetry(label, operation) {
|
|
21
|
+
for (let attempt = 0;; attempt++) {
|
|
22
|
+
try {
|
|
23
|
+
return await operation();
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (!isRateLimited(error) || attempt >= RATE_LIMIT_RETRIES)
|
|
27
|
+
throw error;
|
|
28
|
+
// The server's retry_after_ms only covers one token; tree operations
|
|
29
|
+
// need several (kick + leave + forget). Wait at least 15s so the
|
|
30
|
+
// burst refills.
|
|
31
|
+
let retryAfter = 15_000;
|
|
32
|
+
if (error instanceof Error && "getRetryAfterMs" in error) {
|
|
33
|
+
const getRetryAfterMs = error.getRetryAfterMs;
|
|
34
|
+
if (typeof getRetryAfterMs === "function") {
|
|
35
|
+
const advised = getRetryAfterMs.call(error);
|
|
36
|
+
if (typeof advised === "number" && Number.isFinite(advised) && advised > 0) {
|
|
37
|
+
retryAfter = Math.max(retryAfter, advised);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
await new Promise((resolve) => setTimeout(resolve, retryAfter));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
3
45
|
/**
|
|
4
46
|
* Resolves a folder by ID, polling briefly: a room this same account just
|
|
5
47
|
* created (or was just invited to, by another process/session) can be
|
|
@@ -102,13 +144,15 @@ export async function listPendingInvites(storage) {
|
|
|
102
144
|
export async function declineInvite(storage, folderId) {
|
|
103
145
|
const client = storage.getClient();
|
|
104
146
|
try {
|
|
105
|
-
await
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
147
|
+
await withRateLimitRetry("declineInvite", async () => {
|
|
148
|
+
await client.leave(folderId);
|
|
149
|
+
// The server only allows forgetting a room once this account's
|
|
150
|
+
// membership is `leave`; forgetting removes the room from the local
|
|
151
|
+
// store immediately, so a declined invite stops showing up in
|
|
152
|
+
// listFolders/listPendingInvites without waiting for the leave event
|
|
153
|
+
// to round-trip through the background sync loop.
|
|
154
|
+
await client.forget(folderId);
|
|
155
|
+
});
|
|
112
156
|
}
|
|
113
157
|
catch (err) {
|
|
114
158
|
throw new CliError(`decline failed: ${err.message}`);
|
|
@@ -182,12 +226,14 @@ export async function deleteFolder(storage, folderId) {
|
|
|
182
226
|
// store immediately; the server only accepts a forget once membership is
|
|
183
227
|
// `leave`, which `tree.delete()` has just completed for the whole tree.
|
|
184
228
|
const subdirectoryIds = tree.getDirectories().map((d) => d.id);
|
|
185
|
-
await tree.delete();
|
|
186
229
|
const client = storage.getClient();
|
|
187
|
-
await
|
|
188
|
-
|
|
189
|
-
await client.forget(
|
|
190
|
-
|
|
230
|
+
await withRateLimitRetry("deleteFolder", async () => {
|
|
231
|
+
await tree.delete();
|
|
232
|
+
await client.forget(folderId);
|
|
233
|
+
for (const subId of subdirectoryIds) {
|
|
234
|
+
await client.forget(subId);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
191
237
|
return { id: folderId, deleted: true };
|
|
192
238
|
}
|
|
193
239
|
export async function renameFile(storage, folderId, fileId, name) {
|