@relayfile/sdk 0.10.31 → 0.10.33

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/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export type { RelayFileReadCacheOptions } from "./types.js";
3
3
  export { RelayfileSetup, RELAYFILE_SDK_VERSION, WorkspaceHandle } from "./setup.js";
4
4
  export { type RelayfileCloudLoginOptions, type RelayfileCloudTokenSet, type RelayfileCloudTokenSetupOptions } from "./cloud-login.js";
5
5
  export { CloudAbortError, CloudApiError, CloudTimeoutError, InvalidLocalDirError, InvalidMountModeError, InvalidRemotePathError, IntegrationConnectionTimeoutError, MalformedCloudResponseError, MissingConnectionIdError, MountModeUnavailableError, MountReadyTimeoutError, MountSessionInputError, ProviderNotConnectedError, ProviderNotReadyError, RelayfileSetupError, UnknownProviderError } from "./setup-errors.js";
6
- export { type EnsureMountedWorkspaceInput, WORKSPACE_INTEGRATION_PROVIDERS, type AgentWorkspaceInvite, type AgentWorkspaceInviteOptions, type AgentWorkspaceScopedInviteOptions, type ConnectIntegrationOptions, type ConnectIntegrationResult, type CreateWorkspaceOptions, type JoinWorkspaceOptions, type MountLauncher, type MountLauncherEvent, type MountLauncherInstance, type MountLauncherStart, type MountLocalLayout, type MountMode, type MountSessionRequest, type MountSessionResponse, type MountSessionResult, type MountSyncMode, type MountedWorkspaceHandle, type MountedWorkspaceStatus, type MountWorkspaceInput, type ReadMountedWorkspaceStatusInput, type RelayfileSetupOptions, type RelayfileSetupRetryOptions, type WaitForConnectionOptions, type WorkspaceInfo, type WorkspaceIntegrationProvider, type WorkspaceMountEnv, type WorkspaceMountEnvOptions, type WorkspacePermissions } from "./setup-types.js";
6
+ export { type EnsureMountedWorkspaceInput, WORKSPACE_INTEGRATION_PROVIDERS, type AgentWorkspaceInvite, type AgentWorkspaceInviteOptions, type AgentWorkspaceScopedInviteOptions, type ConnectIntegrationOptions, type ConnectIntegrationResult, type CreateWorkspaceOptions, type JoinWorkspaceOptions, type MountLauncher, type MountLauncherEvent, type MountLauncherInstance, type MountLauncherStart, type MountLocalLayout, type MountMode, type MountSessionRequest, type MountSessionResponse, type MountSessionResult, type MountSyncMode, type MountSupervisorEvent, type MountedWorkspaceHandle, type MountedWorkspaceStatus, type MountWorkspaceInput, type ReadMountedWorkspaceStatusInput, type RelayfileSetupOptions, type RelayfileSetupRetryOptions, type WaitForConnectionOptions, type WorkspaceInfo, type WorkspaceIntegrationProvider, type WorkspaceMountEnv, type WorkspaceMountEnvOptions, type WorkspacePermissions } from "./setup-types.js";
7
7
  export { RelayFileSync, type RelayFileSyncOptions, type RelayFileSyncPong, type RelayFileSyncReconnectOptions, type RelayFileSyncSocket, type RelayFileSyncStart, type RelayFileSyncState, type RelayFileSyncTokenProvider } from "./sync.js";
8
8
  export { onWrite, pathMatches, type OnWriteClient, type OnWriteHandler, type OnWriteHandlerError, type OnWriteOptions } from "./onWrite.js";
9
9
  export { InvalidStateError, PayloadTooLargeError, QueueFullError, RelayFileApiError, RevisionConflictError } from "./errors.js";
@@ -109,7 +109,7 @@ class RelayfileMountProcessInstance {
109
109
  this.ready = this.waitForReady();
110
110
  }
111
111
  async status() {
112
- return readMountedWorkspaceStatus({
112
+ const status = await readMountedWorkspaceStatus({
113
113
  localDir: this.localDir,
114
114
  workspaceId: this.input.env.RELAYFILE_WORKSPACE ?? "",
115
115
  remotePath: this.input.env.RELAYFILE_REMOTE_PATH ?? "/",
@@ -122,6 +122,7 @@ class RelayfileMountProcessInstance {
122
122
  suggestedRefreshAt: null,
123
123
  pid: this.pid
124
124
  });
125
+ return this.exited ? { ...status, ready: false } : status;
125
126
  }
126
127
  async stop() {
127
128
  if (!this.stopping) {
@@ -146,6 +146,14 @@ export interface MountedWorkspaceHandle {
146
146
  status(): Promise<MountedWorkspaceStatus>;
147
147
  stop(): Promise<void>;
148
148
  }
149
+ export interface MountSupervisorEvent {
150
+ type: "mount.refreshed" | "mount.refresh_failed";
151
+ workspaceId: string;
152
+ localDir: string;
153
+ attempt: number;
154
+ retryInMs?: number;
155
+ errorCode?: string;
156
+ }
149
157
  export interface MountLauncherEvent {
150
158
  type: string;
151
159
  [key: string]: unknown;
@@ -186,6 +194,15 @@ export interface EnsureMountedWorkspaceInput extends MountWorkspaceInput {
186
194
  provider?: WorkspaceIntegrationProvider;
187
195
  verifyProvider?: boolean;
188
196
  providerReadyTimeoutMs?: number;
197
+ /** Keep a background mount healthy and renew its short-lived session. Defaults to true. */
198
+ supervise?: boolean;
199
+ /** Health-check cadence for supervised background mounts. */
200
+ healthCheckIntervalMs?: number;
201
+ /** Initial retry delay after a supervised refresh failure. */
202
+ refreshRetryBaseMs?: number;
203
+ /** Maximum retry delay after repeated supervised refresh failures. */
204
+ refreshRetryMaxMs?: number;
205
+ onSupervisorEvent?: (event: MountSupervisorEvent) => void | Promise<void>;
189
206
  }
190
207
  export interface AgentWorkspaceInviteOptions {
191
208
  agentName?: string;
package/dist/setup.d.ts CHANGED
@@ -42,6 +42,7 @@ export declare class RelayfileSetup {
42
42
  private readonly accessToken?;
43
43
  private readonly requestTimeoutMs;
44
44
  private readonly retryOptions;
45
+ private readonly ensuredMounts;
45
46
  static login(options?: RelayfileCloudLoginOptions): Promise<RelayfileSetup>;
46
47
  static fromCloudTokens(tokens: RelayfileCloudTokenSet, options?: RelayfileCloudTokenSetupOptions): RelayfileSetup;
47
48
  constructor(options?: RelayfileSetupOptions);
@@ -49,6 +50,7 @@ export declare class RelayfileSetup {
49
50
  joinWorkspace(workspaceId: string, options?: JoinWorkspaceOptions): Promise<WorkspaceHandle>;
50
51
  mountWorkspace(input: MountWorkspaceInput): Promise<MountedWorkspaceHandle>;
51
52
  ensureMountedWorkspace(input: EnsureMountedWorkspaceInput): Promise<MountedWorkspaceHandle>;
53
+ private startEnsuredMount;
52
54
  joinWorkspaceResponse(workspaceId: string, options: NormalizedJoinWorkspaceOptions, overrides?: {
53
55
  tokenProvider?: AccessTokenProvider;
54
56
  }): Promise<ValidatedJoinWorkspaceResponse>;
package/dist/setup.js CHANGED
@@ -18,6 +18,9 @@ const DEFAULT_MOUNT_READY_TIMEOUT_MS = 60_000;
18
18
  const DEFAULT_MOUNT_AGENT_NAME = "relayfile-mount";
19
19
  const DEFAULT_MOUNT_LOCAL_LAYOUT = "exact";
20
20
  const DEFAULT_MOUNT_SYNC_MODE = "mirror";
21
+ const DEFAULT_MOUNT_HEALTH_INTERVAL_MS = 30_000;
22
+ const DEFAULT_MOUNT_REFRESH_RETRY_BASE_MS = 1_000;
23
+ const DEFAULT_MOUNT_REFRESH_RETRY_MAX_MS = 60_000;
21
24
  const TOKEN_REFRESH_AGE_MS = 55 * 60 * 1000;
22
25
  const nodeOnlyMountLauncher = {
23
26
  async start() {
@@ -29,6 +32,7 @@ export class RelayfileSetup {
29
32
  accessToken;
30
33
  requestTimeoutMs;
31
34
  retryOptions;
35
+ ensuredMounts = new Map();
32
36
  static async login(options = {}) {
33
37
  void options;
34
38
  throw new RelayfileSetupError("RelayfileSetup.login() starts a local HTTP callback server and is only available from @relayfile/sdk/cli. Import RelayfileSetup from @relayfile/sdk/cli for interactive Cloud login.", "node_only_sdk_feature");
@@ -133,13 +137,42 @@ export class RelayfileSetup {
133
137
  async ensureMountedWorkspace(input) {
134
138
  const normalized = normalizeEnsureMountedWorkspaceInput(input);
135
139
  const workspace = await this.resolveWorkspaceForMount(normalized);
140
+ throwIfAborted(normalized.signal, "ensureMountedWorkspace");
141
+ const mountKey = ensuredMountKey(workspace, normalized);
142
+ const existing = this.ensuredMounts.get(mountKey);
143
+ if (existing) {
144
+ return waitForEnsuredMount(existing, normalized.signal);
145
+ }
146
+ let shared;
147
+ // The physical mount belongs to the logical target, not the first caller.
148
+ // Individual abort signals only cancel that caller's wait below.
149
+ shared = this.startEnsuredMount(workspace, {
150
+ ...normalized,
151
+ signal: undefined
152
+ }).then((mounted) => new SharedMountedWorkspaceHandle({
153
+ mounted,
154
+ onStopped: () => {
155
+ if (this.ensuredMounts.get(mountKey) === shared) {
156
+ this.ensuredMounts.delete(mountKey);
157
+ }
158
+ }
159
+ }));
160
+ this.ensuredMounts.set(mountKey, shared);
161
+ void shared.catch(() => {
162
+ if (this.ensuredMounts.get(mountKey) === shared) {
163
+ this.ensuredMounts.delete(mountKey);
164
+ }
165
+ });
166
+ return waitForEnsuredMount(shared, normalized.signal);
167
+ }
168
+ async startEnsuredMount(workspace, normalized) {
136
169
  if (normalized.verifyProvider) {
137
170
  if (!normalized.provider) {
138
171
  throw new MountSessionInputError("provider required when verifyProvider=true");
139
172
  }
140
173
  await verifyWorkspaceProviderReady(workspace, normalized.provider, normalized.providerReadyTimeoutMs, normalized.signal);
141
174
  }
142
- return this.mountWorkspace({
175
+ const mountInput = {
143
176
  workspace,
144
177
  localDir: normalized.localDir,
145
178
  remotePath: normalized.remotePath,
@@ -152,6 +185,19 @@ export class RelayfileSetup {
152
185
  signal: normalized.signal,
153
186
  launcher: normalized.launcher,
154
187
  readyTimeoutMs: normalized.readyTimeoutMs
188
+ };
189
+ const mounted = await this.mountWorkspace(mountInput);
190
+ if (!normalized.supervise || !normalized.background) {
191
+ return mounted;
192
+ }
193
+ return new SupervisedMountedWorkspaceHandle({
194
+ mounted,
195
+ launch: () => this.mountWorkspace(mountInput),
196
+ healthCheckIntervalMs: normalized.healthCheckIntervalMs,
197
+ refreshRetryBaseMs: normalized.refreshRetryBaseMs,
198
+ refreshRetryMaxMs: normalized.refreshRetryMaxMs,
199
+ onEvent: normalized.onSupervisorEvent,
200
+ signal: normalized.signal
155
201
  });
156
202
  }
157
203
  async joinWorkspaceResponse(workspaceId, options, overrides = {}) {
@@ -758,12 +804,256 @@ class MountedWorkspaceHandleImpl {
758
804
  await this.stopPromise;
759
805
  }
760
806
  async performStop() {
807
+ this.readySnapshot = false;
761
808
  if (this.probeOnly || !this.launcherInstance) {
762
809
  return;
763
810
  }
764
811
  await safeStopLauncher(this.launcherInstance);
765
812
  }
766
813
  }
814
+ class SharedMountedWorkspaceHandle {
815
+ mounted;
816
+ onStopped;
817
+ stopPromise;
818
+ constructor(input) {
819
+ this.mounted = input.mounted;
820
+ this.onStopped = input.onStopped;
821
+ }
822
+ get workspaceId() {
823
+ return this.mounted.workspaceId;
824
+ }
825
+ get localDir() {
826
+ return this.mounted.localDir;
827
+ }
828
+ get remotePath() {
829
+ return this.mounted.remotePath;
830
+ }
831
+ get mode() {
832
+ return this.mounted.mode;
833
+ }
834
+ get ready() {
835
+ return this.mounted.ready;
836
+ }
837
+ get expiresAt() {
838
+ return this.mounted.expiresAt;
839
+ }
840
+ get suggestedRefreshAt() {
841
+ return this.mounted.suggestedRefreshAt;
842
+ }
843
+ env() {
844
+ return this.mounted.env();
845
+ }
846
+ status() {
847
+ return this.mounted.status();
848
+ }
849
+ async stop() {
850
+ if (!this.stopPromise)
851
+ this.stopPromise = this.performStop();
852
+ await this.stopPromise;
853
+ }
854
+ async performStop() {
855
+ try {
856
+ await this.mounted.stop();
857
+ }
858
+ finally {
859
+ this.onStopped();
860
+ }
861
+ }
862
+ }
863
+ class SupervisedMountedWorkspaceHandle {
864
+ mounted;
865
+ launch;
866
+ healthCheckIntervalMs;
867
+ refreshRetryBaseMs;
868
+ refreshRetryMaxMs;
869
+ onEvent;
870
+ signal;
871
+ timer;
872
+ refreshPromise;
873
+ stopPromise;
874
+ stopped = false;
875
+ supervisorReady = true;
876
+ refreshAttempts = 0;
877
+ constructor(input) {
878
+ this.mounted = input.mounted;
879
+ this.launch = input.launch;
880
+ this.healthCheckIntervalMs = input.healthCheckIntervalMs;
881
+ this.refreshRetryBaseMs = input.refreshRetryBaseMs;
882
+ this.refreshRetryMaxMs = input.refreshRetryMaxMs;
883
+ this.onEvent = input.onEvent;
884
+ this.signal = input.signal;
885
+ if (this.signal?.aborted) {
886
+ this.handleAbort();
887
+ }
888
+ else {
889
+ this.signal?.addEventListener("abort", this.handleAbort, { once: true });
890
+ this.schedule();
891
+ }
892
+ }
893
+ get workspaceId() {
894
+ return this.mounted.workspaceId;
895
+ }
896
+ get localDir() {
897
+ return this.mounted.localDir;
898
+ }
899
+ get remotePath() {
900
+ return this.mounted.remotePath;
901
+ }
902
+ get mode() {
903
+ return this.mounted.mode;
904
+ }
905
+ get ready() {
906
+ return !this.stopped && this.supervisorReady && this.mounted.ready;
907
+ }
908
+ get expiresAt() {
909
+ return this.mounted.expiresAt;
910
+ }
911
+ get suggestedRefreshAt() {
912
+ return this.mounted.suggestedRefreshAt;
913
+ }
914
+ env() {
915
+ return this.mounted.env();
916
+ }
917
+ async status() {
918
+ const status = await this.mounted.status();
919
+ return {
920
+ ...status,
921
+ ready: !this.stopped && this.supervisorReady && status.ready,
922
+ expiresAt: this.expiresAt,
923
+ suggestedRefreshAt: this.suggestedRefreshAt
924
+ };
925
+ }
926
+ async stop() {
927
+ if (!this.stopPromise)
928
+ this.stopPromise = this.performStop();
929
+ await this.stopPromise;
930
+ }
931
+ async performStop() {
932
+ this.stopped = true;
933
+ this.supervisorReady = false;
934
+ this.signal?.removeEventListener("abort", this.handleAbort);
935
+ if (this.timer)
936
+ clearTimeout(this.timer);
937
+ this.timer = undefined;
938
+ await this.refreshPromise?.catch(() => { });
939
+ await this.mounted.stop();
940
+ }
941
+ handleAbort = () => {
942
+ void this.stop();
943
+ };
944
+ schedule(delayOverrideMs) {
945
+ if (this.stopped || this.timer)
946
+ return;
947
+ const suggestedRefreshAtMs = Date.parse(this.mounted.suggestedRefreshAt ?? "");
948
+ const untilRefresh = Number.isFinite(suggestedRefreshAtMs)
949
+ ? Math.max(0, suggestedRefreshAtMs - Date.now())
950
+ : this.healthCheckIntervalMs;
951
+ const delayMs = delayOverrideMs ?? Math.min(this.healthCheckIntervalMs, untilRefresh);
952
+ this.timer = setTimeout(() => {
953
+ this.timer = undefined;
954
+ this.refreshPromise = this.checkAndRefresh().finally(() => {
955
+ this.refreshPromise = undefined;
956
+ });
957
+ }, Math.max(1_000, delayMs));
958
+ this.timer.unref?.();
959
+ }
960
+ async checkAndRefresh() {
961
+ if (this.stopped)
962
+ return;
963
+ try {
964
+ const suggestedRefreshAtMs = Date.parse(this.mounted.suggestedRefreshAt ?? "");
965
+ const refreshDue = Number.isFinite(suggestedRefreshAtMs) && Date.now() >= suggestedRefreshAtMs;
966
+ if (!refreshDue) {
967
+ const status = await this.mounted.status();
968
+ if (status.ready) {
969
+ this.supervisorReady = true;
970
+ this.refreshAttempts = 0;
971
+ this.schedule();
972
+ return;
973
+ }
974
+ }
975
+ await this.replaceMount();
976
+ if (this.stopped)
977
+ return;
978
+ this.supervisorReady = true;
979
+ this.refreshAttempts = 0;
980
+ this.emit({
981
+ type: "mount.refreshed",
982
+ workspaceId: this.workspaceId,
983
+ localDir: this.localDir,
984
+ attempt: 0
985
+ });
986
+ this.schedule();
987
+ }
988
+ catch (error) {
989
+ if (this.stopped)
990
+ return;
991
+ this.supervisorReady = false;
992
+ this.refreshAttempts += 1;
993
+ const retryInMs = Math.min(this.refreshRetryMaxMs, this.refreshRetryBaseMs * 2 ** (this.refreshAttempts - 1));
994
+ this.emit({
995
+ type: "mount.refresh_failed",
996
+ workspaceId: this.workspaceId,
997
+ localDir: this.localDir,
998
+ attempt: this.refreshAttempts,
999
+ retryInMs,
1000
+ errorCode: setupErrorCode(error)
1001
+ });
1002
+ this.schedule(retryInMs);
1003
+ }
1004
+ }
1005
+ async replaceMount() {
1006
+ const previous = this.mounted;
1007
+ if (this.stopped)
1008
+ return;
1009
+ await previous.stop().catch(() => { });
1010
+ if (this.stopped)
1011
+ return;
1012
+ const replacement = await this.launch();
1013
+ if (this.stopped) {
1014
+ await replacement.stop().catch(() => { });
1015
+ return;
1016
+ }
1017
+ this.mounted = replacement;
1018
+ }
1019
+ emit(event) {
1020
+ void Promise.resolve(this.onEvent?.(event)).catch(() => { });
1021
+ }
1022
+ }
1023
+ function ensuredMountKey(workspace, input) {
1024
+ return JSON.stringify([
1025
+ workspace.workspaceId,
1026
+ input.localDir,
1027
+ input.remotePath,
1028
+ input.mode,
1029
+ input.localLayout,
1030
+ input.syncMode,
1031
+ input.background
1032
+ ]);
1033
+ }
1034
+ async function waitForEnsuredMount(mounted, signal) {
1035
+ if (!signal)
1036
+ return mounted;
1037
+ if (signal.aborted)
1038
+ throw new CloudAbortError("ensureMountedWorkspace");
1039
+ let onAbort;
1040
+ try {
1041
+ return await Promise.race([
1042
+ mounted,
1043
+ new Promise((_, reject) => {
1044
+ onAbort = () => reject(new CloudAbortError("ensureMountedWorkspace"));
1045
+ signal.addEventListener("abort", onAbort, { once: true });
1046
+ })
1047
+ ]);
1048
+ }
1049
+ finally {
1050
+ if (onAbort)
1051
+ signal.removeEventListener("abort", onAbort);
1052
+ }
1053
+ }
1054
+ function setupErrorCode(error) {
1055
+ return error instanceof RelayfileSetupError ? error.code : "mount_refresh_failed";
1056
+ }
767
1057
  // The metadata + accessible-resources verbs accept `WorkspaceIntegrationProvider | string`
768
1058
  // so operators can target dynamically-discovered providers (Composio toolkit
769
1059
  // slugs, future Atlassian-family additions) without an SDK release. We still
@@ -959,9 +1249,19 @@ function normalizeEnsureMountedWorkspaceInput(input) {
959
1249
  ...normalized,
960
1250
  provider: input.provider,
961
1251
  verifyProvider: input.verifyProvider !== false,
962
- providerReadyTimeoutMs: Math.max(0, Math.floor(input.providerReadyTimeoutMs ?? 0))
1252
+ providerReadyTimeoutMs: Math.max(0, Math.floor(input.providerReadyTimeoutMs ?? 0)),
1253
+ supervise: input.supervise !== false && normalized.background,
1254
+ healthCheckIntervalMs: positiveInterval(input.healthCheckIntervalMs, DEFAULT_MOUNT_HEALTH_INTERVAL_MS),
1255
+ refreshRetryBaseMs: positiveInterval(input.refreshRetryBaseMs, DEFAULT_MOUNT_REFRESH_RETRY_BASE_MS),
1256
+ refreshRetryMaxMs: positiveInterval(input.refreshRetryMaxMs, DEFAULT_MOUNT_REFRESH_RETRY_MAX_MS),
1257
+ onSupervisorEvent: input.onSupervisorEvent
963
1258
  };
964
1259
  }
1260
+ function positiveInterval(value, fallback) {
1261
+ if (value === undefined || !Number.isFinite(value) || value <= 0)
1262
+ return fallback;
1263
+ return Math.max(1_000, Math.floor(value));
1264
+ }
965
1265
  function normalizeMountModeInput(mode) {
966
1266
  const normalized = normalizeNonEmptyString(mode) ?? "poll";
967
1267
  if (normalized !== "poll" && normalized !== "fuse") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relayfile/sdk",
3
- "version": "0.10.31",
3
+ "version": "0.10.33",
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.31",
62
+ "@relayfile/core": "0.10.33",
63
63
  "ignore": "^7.0.5",
64
64
  "tar": "^7.5.10"
65
65
  },
66
66
  "optionalDependencies": {
67
- "@relayfile/mount-darwin-arm64": "0.10.31",
68
- "@relayfile/mount-darwin-x64": "0.10.31",
69
- "@relayfile/mount-linux-arm64": "0.10.31",
70
- "@relayfile/mount-linux-x64": "0.10.31"
67
+ "@relayfile/mount-darwin-arm64": "0.10.33",
68
+ "@relayfile/mount-darwin-x64": "0.10.33",
69
+ "@relayfile/mount-linux-arm64": "0.10.33",
70
+ "@relayfile/mount-linux-x64": "0.10.33"
71
71
  },
72
72
  "devDependencies": {
73
73
  "typescript": "^5.7.3",