@relayfile/sdk 0.8.25 → 0.8.27

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/sync.d.ts CHANGED
@@ -106,6 +106,8 @@ export declare class RelayFileSync {
106
106
  private lastFrameAt;
107
107
  private lastPingSentAt;
108
108
  private reconnectAttempts;
109
+ private pollingReprobeAttempts;
110
+ private pollingReprobeEnabled;
109
111
  private readonly abortHandler?;
110
112
  constructor(options: RelayFileSyncOptions);
111
113
  static connect(options: RelayFileSyncOptions): RelayFileSync;
@@ -126,6 +128,7 @@ export declare class RelayFileSync {
126
128
  private forceReconnect;
127
129
  private scheduleReconnect;
128
130
  private computeReconnectDelayMs;
131
+ private computeReprobeDelayMs;
129
132
  private sleep;
130
133
  private isAbortError;
131
134
  private setState;
package/dist/sync.js CHANGED
@@ -44,6 +44,16 @@ const DEFAULT_POLL_INTERVAL_MS = 5000;
44
44
  const DEFAULT_PING_INTERVAL_MS = 30000;
45
45
  const DEFAULT_RECONNECT_MIN_DELAY_MS = 250;
46
46
  const DEFAULT_RECONNECT_MAX_DELAY_MS = 5000;
47
+ // When involuntary polling is active but the WebSocket transport is still
48
+ // viable (baseUrl present, reconnect enabled, not caller-forced), the poll
49
+ // loop yields after a backoff window so the SDK can re-probe the WS and climb
50
+ // back to push delivery. Backoff grows on repeated pre-open failures so we
51
+ // never tight-loop a handshake that keeps rejecting, yet always recover once
52
+ // the transient (cold DO, proxy blip, brief auth gap) clears. Without this a
53
+ // single pre-open failure latches the process into polling forever — fatal
54
+ // for long-running consumers like the factory daemon.
55
+ const DEFAULT_WS_REPROBE_MIN_DELAY_MS = 5000;
56
+ const DEFAULT_WS_REPROBE_MAX_DELAY_MS = 300000;
47
57
  // Cap on the dedupe cache used in polling mode. Sized large enough that no
48
58
  // realistic workspace burst can churn through it within one poll interval
49
59
  // (the events API is itself capped at ~1000 per page), small enough to keep
@@ -202,6 +212,14 @@ export class RelayFileSync {
202
212
  // (no broadcasts and no pings yet) would falsely trip the watchdog.
203
213
  lastPingSentAt = 0;
204
214
  reconnectAttempts = 0;
215
+ // Tracks how many times involuntary polling has yielded to re-probe the
216
+ // WebSocket without a successful open in between. Drives re-probe backoff;
217
+ // reset to 0 once a socket actually reaches OPEN.
218
+ pollingReprobeAttempts = 0;
219
+ // Whether the *current* polling session should periodically yield to
220
+ // re-probe the WS. False for explicit/caller-forced polling (preferPolling
221
+ // or no baseUrl) — that polling is intentional and stays put.
222
+ pollingReprobeEnabled = false;
205
223
  abortHandler;
206
224
  constructor(options) {
207
225
  this.client = options.client;
@@ -399,6 +417,9 @@ export class RelayFileSync {
399
417
  }
400
418
  this.currentSocketHasOpened = true;
401
419
  this.reconnectAttempts = 0;
420
+ // A real OPEN means the transport is healthy again — start the next
421
+ // potential degradation from a fresh (short) re-probe backoff.
422
+ this.pollingReprobeAttempts = 0;
402
423
  // Reset frame/ping bookkeeping for the freshly opened socket so the
403
424
  // watchdog has a clean baseline. lastPingSentAt=0 disables the pong
404
425
  // timeout until we actually send our first ping.
@@ -515,6 +536,15 @@ export class RelayFileSync {
515
536
  }
516
537
  }
517
538
  this.setState("polling");
539
+ // Involuntary polling (anything but "explicit") should not be a one-way
540
+ // door: if the WS transport is viable, the poll loop will yield after a
541
+ // backoff window so the .finally() below can re-open the socket. Explicit
542
+ // polling (preferPolling / no baseUrl) is intentional and stays put.
543
+ this.pollingReprobeEnabled =
544
+ reason !== "explicit" &&
545
+ !!this.baseUrl &&
546
+ this.reconnect.enabled &&
547
+ !this.preferPolling;
518
548
  this.pollingPromise = this.pollLoop().finally(() => {
519
549
  this.pollingPromise = undefined;
520
550
  if (!this.stopped && !this.shouldUsePolling()) {
@@ -524,10 +554,31 @@ export class RelayFileSync {
524
554
  }
525
555
  async pollLoop() {
526
556
  let retryAttempt = 0;
557
+ // When re-probing is enabled, fix a deadline at which this poll session
558
+ // yields back to the WebSocket. Computed once per session from the running
559
+ // attempt count so repeated failures back off (5s, 10s, 20s … capped 5m).
560
+ // `undefined` => never yield (explicit polling, or WS not viable).
561
+ const reprobeDeadlineAt = this.pollingReprobeEnabled
562
+ ? Date.now() + this.computeReprobeDelayMs(this.pollingReprobeAttempts)
563
+ : undefined;
527
564
  while (!this.stopped) {
528
565
  if (this.signal?.aborted) {
529
566
  throw createAbortError();
530
567
  }
568
+ // Backoff window elapsed: exit the loop so startPolling()'s .finally
569
+ // runs scheduleReconnect() → openWebSocket(). The cursor advanced by
570
+ // this session carries into the WS URL, so the re-opened socket resumes
571
+ // exactly where polling left off (no gap, no replay). If the re-open
572
+ // fails pre-open again, forceReconnect → startPolling restarts this loop
573
+ // with an incremented attempt and a longer window.
574
+ if (reprobeDeadlineAt !== undefined && Date.now() >= reprobeDeadlineAt) {
575
+ this.pollingReprobeAttempts += 1;
576
+ debugLog("polling yielding to WS re-probe", {
577
+ workspaceId: this.workspaceId,
578
+ attempt: this.pollingReprobeAttempts,
579
+ });
580
+ return;
581
+ }
531
582
  try {
532
583
  // The current server implementation paginates events from oldest to
533
584
  // newest. Empty cursor starts at index 0, and nextCursor advances
@@ -773,6 +824,14 @@ export class RelayFileSync {
773
824
  const uncapped = this.reconnect.minDelayMs * Math.pow(2, Math.max(0, attempt - 1));
774
825
  return Math.min(this.reconnect.maxDelayMs, uncapped);
775
826
  }
827
+ // How long involuntary polling runs before yielding to re-probe the WS.
828
+ // attempt 0 → 5s, 1 → 10s, 2 → 20s … capped at 5m. The first re-probe after
829
+ // a fresh degradation is fast (transients usually clear within seconds); a
830
+ // persistently rejecting handshake backs off so we never hammer it.
831
+ computeReprobeDelayMs(attempt) {
832
+ const uncapped = DEFAULT_WS_REPROBE_MIN_DELAY_MS * Math.pow(2, Math.max(0, attempt));
833
+ return Math.min(DEFAULT_WS_REPROBE_MAX_DELAY_MS, uncapped);
834
+ }
776
835
  async sleep(delayMs) {
777
836
  if (delayMs <= 0) {
778
837
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relayfile/sdk",
3
- "version": "0.8.25",
3
+ "version": "0.8.27",
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.8.25",
62
+ "@relayfile/core": "0.8.27",
63
63
  "ignore": "^7.0.5",
64
64
  "tar": "^7.5.10"
65
65
  },
66
66
  "optionalDependencies": {
67
- "@relayfile/mount-darwin-arm64": "0.8.25",
68
- "@relayfile/mount-darwin-x64": "0.8.25",
69
- "@relayfile/mount-linux-arm64": "0.8.25",
70
- "@relayfile/mount-linux-x64": "0.8.25"
67
+ "@relayfile/mount-darwin-arm64": "0.8.27",
68
+ "@relayfile/mount-darwin-x64": "0.8.27",
69
+ "@relayfile/mount-linux-arm64": "0.8.27",
70
+ "@relayfile/mount-linux-x64": "0.8.27"
71
71
  },
72
72
  "peerDependencies": {},
73
73
  "devDependencies": {