@relayfile/sdk 0.10.51 → 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 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;
@@ -255,7 +264,9 @@ class RelayFileWebSocketConnection {
255
264
  }
256
265
  });
257
266
  socket.addEventListener("error", (event) => {
258
- const errorEvent = event instanceof ErrorEvent && event.error instanceof Error ? event.error : event;
267
+ const errorEvent = typeof ErrorEvent !== "undefined" && event instanceof ErrorEvent && event.error instanceof Error
268
+ ? event.error
269
+ : event;
259
270
  for (const handler of this.handlers.error) {
260
271
  handler(errorEvent);
261
272
  }
@@ -777,7 +788,14 @@ function getWorkspaceIdFromToken(token) {
777
788
  }
778
789
  try {
779
790
  const parsed = JSON.parse(decodeBase64Url(parts[1] ?? ""));
780
- return typeof parsed.workspace_id === "string" && parsed.workspace_id.length > 0 ? parsed.workspace_id : undefined;
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;
781
799
  }
782
800
  catch {
783
801
  return undefined;
@@ -790,7 +808,15 @@ function getAgentIdFromToken(token) {
790
808
  }
791
809
  try {
792
810
  const parsed = JSON.parse(decodeBase64Url(parts[1] ?? ""));
793
- return typeof parsed.agent_name === "string" && parsed.agent_name.length > 0 ? parsed.agent_name : undefined;
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;
794
820
  }
795
821
  catch {
796
822
  return undefined;
@@ -2177,7 +2203,11 @@ export class RelayFileClient {
2177
2203
  const payload = await this.readPayload(response);
2178
2204
  if (this.shouldRetryStatus(response.status, retries, params.signal)) {
2179
2205
  retries += 1;
2180
- await this.sleep(this.computeRetryDelayMs(retries, response.headers.get("retry-after")), params.signal);
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);
2181
2211
  continue;
2182
2212
  }
2183
2213
  this.throwForError(response.status, payload, response.headers);
@@ -2201,17 +2231,53 @@ export class RelayFileClient {
2201
2231
  }
2202
2232
  return retries < this.retryOptions.maxRetries;
2203
2233
  }
2204
- 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.
2205
2240
  const retryAfterMs = this.parseRetryAfterMs(retryAfterHeader);
2206
2241
  if (retryAfterMs !== null) {
2207
2242
  return Math.min(this.retryOptions.maxDelayMs, retryAfterMs);
2208
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
+ }
2209
2255
  const backoff = this.retryOptions.baseDelayMs * Math.pow(2, Math.max(0, retryAttempt - 1));
2210
2256
  const capped = Math.min(this.retryOptions.maxDelayMs, backoff);
2211
2257
  const jitter = this.retryOptions.jitterRatio;
2212
2258
  const factor = 1 + (Math.random() * 2 - 1) * jitter;
2213
2259
  return Math.max(0, Math.round(capped * factor));
2214
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
+ }
2215
2281
  parseRetryAfterMs(retryAfterHeader) {
2216
2282
  if (!retryAfterHeader) {
2217
2283
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relayfile/sdk",
3
- "version": "0.10.51",
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.51",
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.51",
68
- "@relayfile/mount-darwin-x64": "0.10.51",
69
- "@relayfile/mount-linux-arm64": "0.10.51",
70
- "@relayfile/mount-linux-x64": "0.10.51"
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",