@relayfile/sdk 0.10.50 → 0.10.52
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/dist/client.d.ts +7 -0
- package/dist/client.js +107 -39
- package/package.json +6 -6
package/dist/client.d.ts
CHANGED
|
@@ -178,6 +178,13 @@ export declare class RelayFileClient {
|
|
|
178
178
|
private shouldRetryStatus;
|
|
179
179
|
private shouldRetryError;
|
|
180
180
|
private computeRetryDelayMs;
|
|
181
|
+
/**
|
|
182
|
+
* Extract a server-advertised retry delay from a parsed 429 error body.
|
|
183
|
+
* Relayfile advertises backpressure delays as `details.retryAfterSeconds`
|
|
184
|
+
* (in seconds) on `workspace_busy` / `queue_full` responses. Returns
|
|
185
|
+
* milliseconds, or null when absent/malformed.
|
|
186
|
+
*/
|
|
187
|
+
private parseRetryAfterSecondsFromBody;
|
|
181
188
|
private parseRetryAfterMs;
|
|
182
189
|
private sleep;
|
|
183
190
|
private readPayload;
|
package/dist/client.js
CHANGED
|
@@ -8,6 +8,15 @@ const DEFAULT_RETRY_OPTIONS = {
|
|
|
8
8
|
maxDelayMs: 2000,
|
|
9
9
|
jitterRatio: 0.2
|
|
10
10
|
};
|
|
11
|
+
/**
|
|
12
|
+
* Hard ceiling for a *server-advertised* retry delay (a 429 body's
|
|
13
|
+
* `details.retryAfterSeconds`). `maxDelayMs` bounds our OWN exponential
|
|
14
|
+
* backoff; an explicit server instruction ("retry after N seconds") is honored
|
|
15
|
+
* above that cap — truncating it just retries into the same overloaded resource
|
|
16
|
+
* and burns the retry budget — but is still bounded here so a pathological or
|
|
17
|
+
* hostile value cannot stall the client indefinitely.
|
|
18
|
+
*/
|
|
19
|
+
const RETRY_AFTER_MAX_MS = 30_000;
|
|
11
20
|
const DEFAULT_CHANGE_COALESCE_MS = 200;
|
|
12
21
|
const DEFAULT_CHANGE_LOG_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
13
22
|
const DEFAULT_CHANGE_LOG_MAX_ENTRIES = 10_000;
|
|
@@ -75,6 +84,25 @@ function getFileReadCache(client) {
|
|
|
75
84
|
return cached;
|
|
76
85
|
return false;
|
|
77
86
|
}
|
|
87
|
+
async function withFileReadCacheInvalidated(client, workspaceId, paths, attempt) {
|
|
88
|
+
const cache = getFileReadCache(client);
|
|
89
|
+
if (cache === false)
|
|
90
|
+
return attempt();
|
|
91
|
+
const evict = () => {
|
|
92
|
+
for (const path of paths) {
|
|
93
|
+
cache.evict(workspaceId, path);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
// The attempt itself invalidates our knowledge, before its outcome is known.
|
|
97
|
+
evict();
|
|
98
|
+
try {
|
|
99
|
+
return await attempt();
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
// A concurrent read can repopulate while the request is in flight.
|
|
103
|
+
evict();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
78
106
|
function initFileReadCache(client, options) {
|
|
79
107
|
if (options.readCache === false) {
|
|
80
108
|
fileReadCaches.set(client, false);
|
|
@@ -236,7 +264,9 @@ class RelayFileWebSocketConnection {
|
|
|
236
264
|
}
|
|
237
265
|
});
|
|
238
266
|
socket.addEventListener("error", (event) => {
|
|
239
|
-
const errorEvent =
|
|
267
|
+
const errorEvent = typeof ErrorEvent !== "undefined" && event instanceof ErrorEvent && event.error instanceof Error
|
|
268
|
+
? event.error
|
|
269
|
+
: event;
|
|
240
270
|
for (const handler of this.handlers.error) {
|
|
241
271
|
handler(errorEvent);
|
|
242
272
|
}
|
|
@@ -758,7 +788,14 @@ function getWorkspaceIdFromToken(token) {
|
|
|
758
788
|
}
|
|
759
789
|
try {
|
|
760
790
|
const parsed = JSON.parse(decodeBase64Url(parts[1] ?? ""));
|
|
761
|
-
|
|
791
|
+
// Prefer the canonical `workspace_id` claim; fall back to the `wks` claim
|
|
792
|
+
// minted delegated tokens (relay_pa_*) actually carry, then `workspace`.
|
|
793
|
+
for (const candidate of [parsed.workspace_id, parsed.wks, parsed.workspace]) {
|
|
794
|
+
if (typeof candidate === "string" && candidate.length > 0) {
|
|
795
|
+
return candidate;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return undefined;
|
|
762
799
|
}
|
|
763
800
|
catch {
|
|
764
801
|
return undefined;
|
|
@@ -771,7 +808,15 @@ function getAgentIdFromToken(token) {
|
|
|
771
808
|
}
|
|
772
809
|
try {
|
|
773
810
|
const parsed = JSON.parse(decodeBase64Url(parts[1] ?? ""));
|
|
774
|
-
|
|
811
|
+
if (typeof parsed.agent_name === "string" && parsed.agent_name.length > 0) {
|
|
812
|
+
return parsed.agent_name;
|
|
813
|
+
}
|
|
814
|
+
// Minted delegated tokens omit `agent_name` and carry the agent as the JWT
|
|
815
|
+
// subject, e.g. sub: "agent_gil-ramp-bookkeeper".
|
|
816
|
+
if (typeof parsed.sub === "string" && parsed.sub.startsWith("agent_") && parsed.sub.length > "agent_".length) {
|
|
817
|
+
return parsed.sub.slice("agent_".length);
|
|
818
|
+
}
|
|
819
|
+
return undefined;
|
|
775
820
|
}
|
|
776
821
|
catch {
|
|
777
822
|
return undefined;
|
|
@@ -1248,7 +1293,7 @@ export class RelayFileClient {
|
|
|
1248
1293
|
async writeFile(input) {
|
|
1249
1294
|
const { workspaceId, path, correlationId, baseRevision, content, contentType, encoding, contentIdentity, signal } = input;
|
|
1250
1295
|
const query = buildQuery({ path, forkId: input.forkId });
|
|
1251
|
-
|
|
1296
|
+
return withFileReadCacheInvalidated(this, workspaceId, [path], () => this.request({
|
|
1252
1297
|
method: "PUT",
|
|
1253
1298
|
path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/fs/file${query}`,
|
|
1254
1299
|
correlationId,
|
|
@@ -1264,15 +1309,11 @@ export class RelayFileClient {
|
|
|
1264
1309
|
...(contentIdentity ? { contentIdentity } : {})
|
|
1265
1310
|
},
|
|
1266
1311
|
signal
|
|
1267
|
-
});
|
|
1268
|
-
const cache = getFileReadCache(this);
|
|
1269
|
-
if (cache !== false)
|
|
1270
|
-
cache.evict(workspaceId, path);
|
|
1271
|
-
return result;
|
|
1312
|
+
}));
|
|
1272
1313
|
}
|
|
1273
1314
|
async mergeFile(input) {
|
|
1274
1315
|
const query = buildQuery({ path: input.path });
|
|
1275
|
-
|
|
1316
|
+
return withFileReadCacheInvalidated(this, input.workspaceId, [input.path], () => this.request({
|
|
1276
1317
|
method: "POST",
|
|
1277
1318
|
path: `/v1/workspaces/${encodeURIComponent(input.workspaceId)}/fs/merge${query}`,
|
|
1278
1319
|
correlationId: input.correlationId,
|
|
@@ -1291,31 +1332,22 @@ export class RelayFileClient {
|
|
|
1291
1332
|
contentIdentity: input.contentIdentity
|
|
1292
1333
|
},
|
|
1293
1334
|
signal: input.signal
|
|
1294
|
-
});
|
|
1295
|
-
const cache = getFileReadCache(this);
|
|
1296
|
-
if (cache !== false)
|
|
1297
|
-
cache.evict(input.workspaceId, input.path);
|
|
1298
|
-
return result;
|
|
1335
|
+
}));
|
|
1299
1336
|
}
|
|
1300
1337
|
async bulkWrite(input) {
|
|
1301
1338
|
const query = buildQuery({ forkId: input.forkId });
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1339
|
+
return withFileReadCacheInvalidated(this, input.workspaceId, input.files.map((file) => file.path), async () => {
|
|
1340
|
+
const response = await this.performRequest({
|
|
1341
|
+
method: "POST",
|
|
1342
|
+
path: `/v1/workspaces/${encodeURIComponent(input.workspaceId)}/fs/bulk${query}`,
|
|
1343
|
+
correlationId: input.correlationId,
|
|
1344
|
+
body: {
|
|
1345
|
+
files: input.files
|
|
1346
|
+
},
|
|
1347
|
+
signal: input.signal
|
|
1348
|
+
});
|
|
1349
|
+
return await this.readPayload(response);
|
|
1310
1350
|
});
|
|
1311
|
-
const result = await this.readPayload(response);
|
|
1312
|
-
const cache = getFileReadCache(this);
|
|
1313
|
-
if (cache !== false) {
|
|
1314
|
-
for (const file of input.files) {
|
|
1315
|
-
cache.evict(input.workspaceId, file.path);
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
return result;
|
|
1319
1351
|
}
|
|
1320
1352
|
async issueCheckpointSeal(input) {
|
|
1321
1353
|
return this.request({
|
|
@@ -1420,7 +1452,7 @@ export class RelayFileClient {
|
|
|
1420
1452
|
}
|
|
1421
1453
|
async deleteFile(input) {
|
|
1422
1454
|
const query = buildQuery({ path: input.path, forkId: input.forkId });
|
|
1423
|
-
|
|
1455
|
+
return withFileReadCacheInvalidated(this, input.workspaceId, [input.path], () => this.request({
|
|
1424
1456
|
method: "DELETE",
|
|
1425
1457
|
path: `/v1/workspaces/${encodeURIComponent(input.workspaceId)}/fs/file${query}`,
|
|
1426
1458
|
correlationId: input.correlationId,
|
|
@@ -1428,11 +1460,7 @@ export class RelayFileClient {
|
|
|
1428
1460
|
"If-Match": input.baseRevision
|
|
1429
1461
|
},
|
|
1430
1462
|
signal: input.signal
|
|
1431
|
-
});
|
|
1432
|
-
const cache = getFileReadCache(this);
|
|
1433
|
-
if (cache !== false)
|
|
1434
|
-
cache.evict(input.workspaceId, input.path);
|
|
1435
|
-
return result;
|
|
1463
|
+
}));
|
|
1436
1464
|
}
|
|
1437
1465
|
async createFork(input) {
|
|
1438
1466
|
const body = {
|
|
@@ -2175,7 +2203,11 @@ export class RelayFileClient {
|
|
|
2175
2203
|
const payload = await this.readPayload(response);
|
|
2176
2204
|
if (this.shouldRetryStatus(response.status, retries, params.signal)) {
|
|
2177
2205
|
retries += 1;
|
|
2178
|
-
await this.sleep(this.computeRetryDelayMs(retries, response.headers.get("retry-after")
|
|
2206
|
+
await this.sleep(this.computeRetryDelayMs(retries, response.headers.get("retry-after"),
|
|
2207
|
+
// `details.retryAfterSeconds` is a 429-only backpressure signal
|
|
2208
|
+
// (`workspace_busy` / `queue_full`). Only consult the body on a 429
|
|
2209
|
+
// so a 5xx body can never bypass `maxDelayMs` via this path.
|
|
2210
|
+
response.status === 429 ? payload : undefined), params.signal);
|
|
2179
2211
|
continue;
|
|
2180
2212
|
}
|
|
2181
2213
|
this.throwForError(response.status, payload, response.headers);
|
|
@@ -2199,17 +2231,53 @@ export class RelayFileClient {
|
|
|
2199
2231
|
}
|
|
2200
2232
|
return retries < this.retryOptions.maxRetries;
|
|
2201
2233
|
}
|
|
2202
|
-
computeRetryDelayMs(retryAttempt, retryAfterHeader) {
|
|
2234
|
+
computeRetryDelayMs(retryAttempt, retryAfterHeader, payload) {
|
|
2235
|
+
// A server-advertised `Retry-After` HEADER is the standard, explicit
|
|
2236
|
+
// signal and takes precedence: parse it first and leave its handling
|
|
2237
|
+
// unchanged (bounded by our own `maxDelayMs`). Only when the header is
|
|
2238
|
+
// absent or unparseable do we consult the body below, so a body hint never
|
|
2239
|
+
// silently overrides a shorter, explicit header the server already sent.
|
|
2203
2240
|
const retryAfterMs = this.parseRetryAfterMs(retryAfterHeader);
|
|
2204
2241
|
if (retryAfterMs !== null) {
|
|
2205
2242
|
return Math.min(this.retryOptions.maxDelayMs, retryAfterMs);
|
|
2206
2243
|
}
|
|
2244
|
+
// With no usable header, a 429 body can still advertise an explicit
|
|
2245
|
+
// backpressure delay as `details.retryAfterSeconds` (e.g. `workspace_busy`
|
|
2246
|
+
// when the workspace durable object is overloaded, or `queue_full`). Honor
|
|
2247
|
+
// it as an instruction, bounded only by RETRY_AFTER_MAX_MS — NOT truncated
|
|
2248
|
+
// to `maxDelayMs`, which governs our own exponential backoff. Truncating it
|
|
2249
|
+
// (maxDelayMs defaults to 2s vs. a typical 5s advertised delay) retries
|
|
2250
|
+
// into the still-busy resource and exhausts the retry budget.
|
|
2251
|
+
const advertisedMs = this.parseRetryAfterSecondsFromBody(payload);
|
|
2252
|
+
if (advertisedMs !== null) {
|
|
2253
|
+
return Math.max(0, Math.min(RETRY_AFTER_MAX_MS, advertisedMs));
|
|
2254
|
+
}
|
|
2207
2255
|
const backoff = this.retryOptions.baseDelayMs * Math.pow(2, Math.max(0, retryAttempt - 1));
|
|
2208
2256
|
const capped = Math.min(this.retryOptions.maxDelayMs, backoff);
|
|
2209
2257
|
const jitter = this.retryOptions.jitterRatio;
|
|
2210
2258
|
const factor = 1 + (Math.random() * 2 - 1) * jitter;
|
|
2211
2259
|
return Math.max(0, Math.round(capped * factor));
|
|
2212
2260
|
}
|
|
2261
|
+
/**
|
|
2262
|
+
* Extract a server-advertised retry delay from a parsed 429 error body.
|
|
2263
|
+
* Relayfile advertises backpressure delays as `details.retryAfterSeconds`
|
|
2264
|
+
* (in seconds) on `workspace_busy` / `queue_full` responses. Returns
|
|
2265
|
+
* milliseconds, or null when absent/malformed.
|
|
2266
|
+
*/
|
|
2267
|
+
parseRetryAfterSecondsFromBody(payload) {
|
|
2268
|
+
if (!payload || typeof payload !== "object") {
|
|
2269
|
+
return null;
|
|
2270
|
+
}
|
|
2271
|
+
const details = payload.details;
|
|
2272
|
+
if (!details || typeof details !== "object") {
|
|
2273
|
+
return null;
|
|
2274
|
+
}
|
|
2275
|
+
const seconds = details.retryAfterSeconds;
|
|
2276
|
+
if (typeof seconds === "number" && Number.isFinite(seconds) && seconds >= 0) {
|
|
2277
|
+
return seconds * 1000;
|
|
2278
|
+
}
|
|
2279
|
+
return null;
|
|
2280
|
+
}
|
|
2213
2281
|
parseRetryAfterMs(retryAfterHeader) {
|
|
2214
2282
|
if (!retryAfterHeader) {
|
|
2215
2283
|
return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relayfile/sdk",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.52",
|
|
4
4
|
"description": "TypeScript SDK for relayfile — real-time filesystem for humans and agents",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -59,15 +59,15 @@
|
|
|
59
59
|
"prepublishOnly": "npm run build"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@relayfile/core": "0.10.
|
|
62
|
+
"@relayfile/core": "0.10.52",
|
|
63
63
|
"ignore": "^7.0.5",
|
|
64
64
|
"tar": "^7.5.10"
|
|
65
65
|
},
|
|
66
66
|
"optionalDependencies": {
|
|
67
|
-
"@relayfile/mount-darwin-arm64": "0.10.
|
|
68
|
-
"@relayfile/mount-darwin-x64": "0.10.
|
|
69
|
-
"@relayfile/mount-linux-arm64": "0.10.
|
|
70
|
-
"@relayfile/mount-linux-x64": "0.10.
|
|
67
|
+
"@relayfile/mount-darwin-arm64": "0.10.52",
|
|
68
|
+
"@relayfile/mount-darwin-x64": "0.10.52",
|
|
69
|
+
"@relayfile/mount-linux-arm64": "0.10.52",
|
|
70
|
+
"@relayfile/mount-linux-x64": "0.10.52"
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"typescript": "^5.7.3",
|