@telecrypt-io/storage 0.2.3 → 0.2.5

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 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.3` is the current public release,
12
- published from this repository's protected `v0.2.3` tag with NPM Trusted Publishing/OIDC
11
+ **Status:** library source only. `@telecrypt-io/storage@0.2.5` is the current public release,
12
+ published from this repository's protected `v0.2.5` 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.3
18
+ npm install @telecrypt-io/storage@0.2.5
19
19
  ```
20
20
 
21
21
  This gives you the `TeleCryptIOStorage` library and its browser-safe `core` API.
@@ -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
@@ -32,7 +74,7 @@ function toArrayBuffer(bytes) {
32
74
  return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
33
75
  }
34
76
  export async function createFolder(storage, name) {
35
- const tree = await storage.createTree(name);
77
+ const tree = await withRateLimitRetry("createFolder", () => storage.createTree(name));
36
78
  return { id: tree.id, name };
37
79
  }
38
80
  /** Top-level folders only — excludes subdirectories of an existing tree. */
@@ -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 client.leave(folderId);
106
- // The server only allows forgetting a room once this account's
107
- // membership is `leave`; forgetting removes the room from the local
108
- // store immediately, so a declined invite stops showing up in
109
- // listFolders/listPendingInvites without waiting for the leave event to
110
- // round-trip through the background sync loop.
111
- await client.forget(folderId);
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}`);
@@ -164,12 +208,12 @@ export async function listSubfolders(storage, folderId) {
164
208
  }
165
209
  export async function createSubfolder(storage, folderId, name) {
166
210
  const tree = await resolveTree(storage, folderId);
167
- const sub = await tree.createDirectory(name);
211
+ const sub = await withRateLimitRetry("createSubfolder", () => tree.createDirectory(name));
168
212
  return { id: sub.id, name };
169
213
  }
170
214
  export async function renameFolder(storage, folderId, name) {
171
215
  const tree = await resolveTree(storage, folderId);
172
- await tree.setName(name);
216
+ await withRateLimitRetry("renameFolder", () => tree.setName(name));
173
217
  return { id: folderId, name };
174
218
  }
175
219
  export async function deleteFolder(storage, folderId) {
@@ -182,18 +226,20 @@ 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 client.forget(folderId);
188
- for (const subId of subdirectoryIds) {
189
- await client.forget(subId);
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) {
194
240
  const tree = await resolveTree(storage, folderId);
195
241
  const branch = await resolveFile(tree, fileId);
196
- await branch.setName(name);
242
+ await withRateLimitRetry("renameFile", () => branch.setName(name));
197
243
  // `setName` resolves when the homeserver accepts the state event, but a
198
244
  // fresh CLI/UI process can still read the previous local room state for a
199
245
  // short time. Do not report success until this client has observed the new
@@ -207,12 +253,12 @@ export async function renameFile(storage, folderId, fileId, name) {
207
253
  export async function deleteFile(storage, folderId, fileId) {
208
254
  const tree = await resolveTree(storage, folderId);
209
255
  const branch = await resolveFile(tree, fileId);
210
- await branch.delete();
256
+ await withRateLimitRetry("deleteFile", () => branch.delete());
211
257
  return { id: fileId, deleted: true };
212
258
  }
213
259
  export async function uploadFile(storage, folderId, name, bytes, mimetype) {
214
260
  const tree = await resolveTree(storage, folderId);
215
- const fileId = await storage.uploadFile(tree, name, toArrayBuffer(bytes), mimetype);
261
+ const fileId = await withRateLimitRetry("uploadFile", () => storage.uploadFile(tree, name, toArrayBuffer(bytes), mimetype));
216
262
  return { id: fileId, name, mimetype };
217
263
  }
218
264
  export async function downloadFile(storage, folderId, fileId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telecrypt-io/storage",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "license": "BUSL-1.1",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",