@relayfile/sdk 0.8.9 → 0.8.11

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
@@ -55,6 +55,9 @@ export interface WebSocketConnection {
55
55
  }
56
56
  export interface ConnectWebSocketOptions {
57
57
  token?: string;
58
+ from?: "now" | "legacy";
59
+ cursor?: string;
60
+ paths?: string[];
58
61
  onEvent?: (event: FilesystemEvent) => void;
59
62
  }
60
63
  interface ProactiveRequestContext {
package/dist/client.js CHANGED
@@ -318,6 +318,14 @@ class RelayFileChangeSubscription {
318
318
  }
319
319
  await drain;
320
320
  }
321
+ serverPathFilters() {
322
+ const filters = new Set();
323
+ const patterns = this.pathScopes ?? this.globPatterns;
324
+ for (const pattern of patterns) {
325
+ filters.add(`/${pattern.join("/")}`);
326
+ }
327
+ return Array.from(filters);
328
+ }
321
329
  matches(path) {
322
330
  const pathSegments = normalizeChangePath(path);
323
331
  const matchesGlob = this.globPatterns.some((pattern) => matchChangeSegments(pattern, pathSegments));
@@ -353,18 +361,21 @@ class RelayFileChangeStreamManager {
353
361
  workspaceId;
354
362
  token;
355
363
  baseUrl;
364
+ startOptions;
356
365
  subscriptions = new Set();
357
366
  openHandleCount = 0;
358
367
  sync;
368
+ activePathFilterKey;
359
369
  readyResolved = false;
360
370
  readyInternal;
361
371
  resolveReady;
362
372
  rejectReady;
363
- constructor(client, workspaceId, token, baseUrl) {
373
+ constructor(client, workspaceId, token, baseUrl, startOptions) {
364
374
  this.client = client;
365
375
  this.workspaceId = workspaceId;
366
376
  this.token = token;
367
377
  this.baseUrl = baseUrl;
378
+ this.startOptions = startOptions;
368
379
  this.readyInternal = new Promise((resolve, reject) => {
369
380
  this.resolveReady = resolve;
370
381
  this.rejectReady = reject;
@@ -376,22 +387,26 @@ class RelayFileChangeStreamManager {
376
387
  addSubscription(globs, onChange, options) {
377
388
  const subscription = new RelayFileChangeSubscription(this, globs, onChange, options);
378
389
  this.subscriptions.add(subscription);
390
+ this.restartIfPathScopeChanged();
379
391
  this.ensureStarted();
380
392
  return {
381
393
  unsubscribe: async () => {
382
394
  this.subscriptions.delete(subscription);
383
395
  await subscription.close();
396
+ this.restartIfPathScopeChanged();
384
397
  await this.maybeStop();
385
398
  }
386
399
  };
387
400
  }
388
401
  open() {
389
402
  this.openHandleCount += 1;
403
+ this.restartIfPathScopeChanged();
390
404
  this.ensureStarted();
391
405
  return {
392
406
  ready: this.ready,
393
407
  unsubscribe: async () => {
394
408
  this.openHandleCount = Math.max(0, this.openHandleCount - 1);
409
+ this.restartIfPathScopeChanged();
395
410
  await this.maybeStop();
396
411
  }
397
412
  };
@@ -429,11 +444,16 @@ class RelayFileChangeStreamManager {
429
444
  if (this.sync) {
430
445
  return;
431
446
  }
447
+ const paths = this.serverPathFilters();
448
+ this.activePathFilterKey = paths.join("\n");
432
449
  const sync = new RelayFileSync({
433
450
  client: this.client,
434
451
  workspaceId: this.workspaceId,
435
452
  baseUrl: this.baseUrl,
436
453
  token: this.token,
454
+ from: this.startOptions.from,
455
+ cursor: this.startOptions.cursor,
456
+ paths,
437
457
  onPollingFallback: () => {
438
458
  this.resolveReadyOnce();
439
459
  }
@@ -462,6 +482,34 @@ class RelayFileChangeStreamManager {
462
482
  this.resolveReadyOnce();
463
483
  }
464
484
  }
485
+ restartIfPathScopeChanged() {
486
+ if (!this.sync) {
487
+ return;
488
+ }
489
+ const nextKey = this.serverPathFilters().join("\n");
490
+ const currentKey = this.activePathFilterKey;
491
+ if (nextKey === currentKey) {
492
+ return;
493
+ }
494
+ const sync = this.sync;
495
+ this.sync = undefined;
496
+ void sync.stop();
497
+ if (this.openHandleCount > 0 || this.subscriptions.size > 0) {
498
+ this.ensureStarted();
499
+ }
500
+ }
501
+ serverPathFilters() {
502
+ if (this.openHandleCount > 0) {
503
+ return [];
504
+ }
505
+ const filters = new Set();
506
+ for (const subscription of this.subscriptions) {
507
+ for (const path of subscription.serverPathFilters()) {
508
+ filters.add(path);
509
+ }
510
+ }
511
+ return Array.from(filters).sort();
512
+ }
465
513
  resolveReadyOnce() {
466
514
  if (this.readyResolved) {
467
515
  return;
@@ -476,6 +524,7 @@ class RelayFileChangeStreamManager {
476
524
  if (this.sync) {
477
525
  const sync = this.sync;
478
526
  this.sync = undefined;
527
+ this.activePathFilterKey = undefined;
479
528
  await sync.stop();
480
529
  }
481
530
  const managers = changeStreamManagers.get(this.client);
@@ -489,18 +538,21 @@ class RelayFileChangeStreamManager {
489
538
  }
490
539
  }
491
540
  }
492
- function getStreamManager(client, workspaceId, token, baseUrl) {
541
+ function getStreamManager(client, workspaceId, token, baseUrl, startOptions = {}) {
493
542
  let managers = changeStreamManagers.get(client);
494
543
  if (!managers) {
495
544
  managers = new Map();
496
545
  changeStreamManagers.set(client, managers);
497
546
  }
498
- const key = `${workspaceId}:${token ?? CLIENT_TOKEN_STREAM_KEY}`;
547
+ const key = `${workspaceId}:${token ?? CLIENT_TOKEN_STREAM_KEY}:${startOptions.from ?? "now"}:${startOptions.cursor ?? ""}`;
499
548
  const existing = managers.get(key);
500
549
  if (existing) {
501
550
  return existing;
502
551
  }
503
- const manager = new RelayFileChangeStreamManager(client, workspaceId, token, baseUrl);
552
+ const manager = new RelayFileChangeStreamManager(client, workspaceId, token, baseUrl, {
553
+ from: startOptions.from,
554
+ cursor: startOptions.cursor
555
+ });
504
556
  managers.set(key, manager);
505
557
  return manager;
506
558
  }
@@ -1125,7 +1177,7 @@ export class RelayFileClient {
1125
1177
  subscribe(globs, onChange, options) {
1126
1178
  const setup = this.resolveWorkspaceId(options?.aclToken)
1127
1179
  .then((workspaceId) => {
1128
- const manager = getStreamManager(this, workspaceId, options?.aclToken, this.baseUrl);
1180
+ const manager = getStreamManager(this, workspaceId, options?.aclToken, this.baseUrl, options);
1129
1181
  return manager.addSubscription(globs, onChange, options);
1130
1182
  });
1131
1183
  return {
@@ -1136,7 +1188,7 @@ export class RelayFileClient {
1136
1188
  };
1137
1189
  }
1138
1190
  open(options) {
1139
- const manager = getStreamManager(this, options.workspaceId, options.aclToken, this.baseUrl);
1191
+ const manager = getStreamManager(this, options.workspaceId, options.aclToken, this.baseUrl, options);
1140
1192
  const connection = manager.open();
1141
1193
  const replay = this.primeReplayCache(options).catch((error) => {
1142
1194
  if (typeof console !== "undefined" && typeof console.error === "function") {
@@ -1252,6 +1304,15 @@ export class RelayFileClient {
1252
1304
  const url = new URL(`${this.baseUrl}/v1/workspaces/${encodeURIComponent(workspaceId)}/fs/ws`);
1253
1305
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
1254
1306
  url.searchParams.set("token", token);
1307
+ if (options.cursor) {
1308
+ url.searchParams.set("cursor", options.cursor);
1309
+ }
1310
+ else if ((options.from ?? "now") === "now") {
1311
+ url.searchParams.set("from", "now");
1312
+ }
1313
+ for (const path of options.paths ?? []) {
1314
+ url.searchParams.append("path", path);
1315
+ }
1255
1316
  const socket = new WebSocket(url.toString());
1256
1317
  return new RelayFileWebSocketConnection(socket, options.onEvent);
1257
1318
  }
package/dist/index.d.ts CHANGED
@@ -2,8 +2,8 @@ export { RelayFileClient, DEFAULT_RELAYFILE_BASE_URL, type AccessTokenProvider,
2
2
  export { RelayfileSetup, RELAYFILE_SDK_VERSION, WorkspaceHandle } from "./setup.js";
3
3
  export { type RelayfileCloudLoginOptions, type RelayfileCloudTokenSet, type RelayfileCloudTokenSetupOptions } from "./cloud-login.js";
4
4
  export { CloudAbortError, CloudApiError, CloudTimeoutError, InvalidLocalDirError, InvalidMountModeError, InvalidRemotePathError, IntegrationConnectionTimeoutError, MalformedCloudResponseError, MissingConnectionIdError, MountModeUnavailableError, MountReadyTimeoutError, MountSessionInputError, ProviderNotConnectedError, ProviderNotReadyError, RelayfileSetupError, UnknownProviderError } from "./setup-errors.js";
5
- 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 MountMode, type MountSessionRequest, type MountSessionResponse, type MountSessionResult, 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 { RelayFileSync, type RelayFileSyncOptions, type RelayFileSyncPong, type RelayFileSyncReconnectOptions, type RelayFileSyncSocket, type RelayFileSyncState, type RelayFileSyncTokenProvider } from "./sync.js";
5
+ 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 { RelayFileSync, type RelayFileSyncOptions, type RelayFileSyncPong, type RelayFileSyncReconnectOptions, type RelayFileSyncSocket, type RelayFileSyncStart, type RelayFileSyncState, type RelayFileSyncTokenProvider } from "./sync.js";
7
7
  export { onWrite, pathMatches, type OnWriteClient, type OnWriteHandler, type OnWriteHandlerError, type OnWriteOptions } from "./onWrite.js";
8
8
  export { InvalidStateError, PayloadTooLargeError, QueueFullError, RelayFileApiError, RevisionConflictError } from "./errors.js";
9
9
  export { IntegrationProvider, computeCanonicalPath } from "./provider.js";
@@ -20,7 +20,7 @@ export function createDefaultMountLauncher(options = {}) {
20
20
  };
21
21
  }
22
22
  export async function readMountedWorkspaceStatus(input) {
23
- const state = await readMountStateFile(input.localDir);
23
+ const state = await readMountStateFile(resolveMountLocalDir(input.localDir, input.remotePath, input.localLayout));
24
24
  if (state && !isMountStateStale(state)) {
25
25
  return {
26
26
  ready: isMountStateReady(state),
@@ -46,7 +46,8 @@ export async function readMountedWorkspaceStatus(input) {
46
46
  }
47
47
  async function startRelayfileMount(input, options) {
48
48
  const localDir = path.resolve(input.env.RELAYFILE_LOCAL_DIR ?? process.cwd());
49
- const relayDir = path.join(localDir, ".relay");
49
+ const mountLocalDir = resolveMountLocalDir(localDir, input.env.RELAYFILE_REMOTE_PATH, input.env.RELAYFILE_MOUNT_LOCAL_LAYOUT);
50
+ const relayDir = path.join(mountLocalDir, ".relay");
50
51
  const logPath = path.join(relayDir, "mount.log");
51
52
  const pidPath = path.join(relayDir, "mount.pid");
52
53
  await mkdir(relayDir, { recursive: true });
@@ -54,7 +55,7 @@ async function startRelayfileMount(input, options) {
54
55
  const command = await resolveRelayfileMountCommand();
55
56
  const args = input.background === false ? ["--once"] : [];
56
57
  const child = (options.spawnImpl ?? spawn)(command, args, {
57
- cwd: input.cwd ?? localDir,
58
+ cwd: input.cwd ?? mountLocalDir,
58
59
  env: {
59
60
  ...process.env,
60
61
  ...input.env
@@ -73,7 +74,7 @@ async function startRelayfileMount(input, options) {
73
74
  pidPath,
74
75
  outputBuffer,
75
76
  input,
76
- localDir,
77
+ localDir: mountLocalDir,
77
78
  now: options.now ?? Date.now,
78
79
  readyPollIntervalMs: options.readyPollIntervalMs ?? DEFAULT_READY_POLL_INTERVAL_MS
79
80
  });
@@ -113,6 +114,8 @@ class RelayfileMountProcessInstance {
113
114
  workspaceId: this.input.env.RELAYFILE_WORKSPACE ?? "",
114
115
  remotePath: this.input.env.RELAYFILE_REMOTE_PATH ?? "/",
115
116
  mode: normalizeMountMode(this.input.env.RELAYFILE_MOUNT_MODE) ?? "poll",
117
+ localLayout: normalizeMountLocalLayout(this.input.env.RELAYFILE_MOUNT_LOCAL_LAYOUT),
118
+ syncMode: normalizeMountSyncMode(this.input.env.RELAYFILE_MOUNT_SYNC_MODE),
116
119
  relayfileBaseUrl: this.input.env.RELAYFILE_BASE_URL ?? "",
117
120
  relayfileToken: this.input.env.RELAYFILE_TOKEN ?? "",
118
121
  expiresAt: null,
@@ -251,6 +254,32 @@ function isMountStateStale(state) {
251
254
  function normalizeMountMode(mode) {
252
255
  return mode === "fuse" ? "fuse" : mode === "poll" ? "poll" : undefined;
253
256
  }
257
+ function normalizeMountLocalLayout(layout) {
258
+ return layout === "scoped" ? "scoped" : "exact";
259
+ }
260
+ function normalizeMountSyncMode(mode) {
261
+ return mode === "write-only" ? "write-only" : "mirror";
262
+ }
263
+ function resolveMountLocalDir(localDir, remotePath, localLayout) {
264
+ const root = path.resolve(localDir);
265
+ if (normalizeMountLocalLayout(localLayout) !== "scoped") {
266
+ return root;
267
+ }
268
+ const normalizedRemote = normalizeRemotePath(remotePath);
269
+ if (normalizedRemote === "/") {
270
+ return root;
271
+ }
272
+ return path.join(root, ...normalizedRemote.split("/").filter(Boolean));
273
+ }
274
+ function normalizeRemotePath(remotePath) {
275
+ const trimmed = typeof remotePath === "string" ? remotePath.trim() : "";
276
+ if (!trimmed || trimmed === "/") {
277
+ return "/";
278
+ }
279
+ const slashNormalized = trimmed.replace(/\\/g, "/");
280
+ const normalized = path.posix.normalize(slashNormalized.startsWith("/") ? slashNormalized : `/${slashNormalized}`);
281
+ return normalized === "/" ? "/" : normalized.replace(/\/+$/, "");
282
+ }
254
283
  function normalizeIsoString(value) {
255
284
  if (typeof value !== "string" || value.trim() === "") {
256
285
  return undefined;
@@ -67,6 +67,8 @@ export interface WorkspaceMountEnvOptions {
67
67
  }
68
68
  export type WorkspaceMountEnv = Record<string, string>;
69
69
  export type MountMode = "poll" | "fuse";
70
+ export type MountLocalLayout = "exact" | "scoped";
71
+ export type MountSyncMode = "mirror" | "write-only";
70
72
  export interface MountSessionRequest {
71
73
  localDir: string;
72
74
  remotePath?: string;
@@ -98,6 +100,8 @@ export interface MountSessionResult {
98
100
  remotePath: string;
99
101
  localDir: string;
100
102
  mode: MountMode;
103
+ localLayout: MountLocalLayout;
104
+ syncMode: MountSyncMode;
101
105
  scopes: string[];
102
106
  tokenIssuedAt: string | null;
103
107
  expiresAt: string | null;
@@ -122,6 +126,8 @@ export interface ReadMountedWorkspaceStatusInput {
122
126
  workspaceId: string;
123
127
  remotePath: string;
124
128
  mode: MountMode;
129
+ localLayout?: MountLocalLayout;
130
+ syncMode?: MountSyncMode;
125
131
  relayfileBaseUrl: string;
126
132
  relayfileToken: string;
127
133
  expiresAt: string | null;
@@ -167,6 +173,8 @@ export interface MountWorkspaceInput {
167
173
  localDir: string;
168
174
  remotePath?: string;
169
175
  mode?: MountMode;
176
+ localLayout?: MountLocalLayout;
177
+ syncMode?: MountSyncMode;
170
178
  background?: boolean;
171
179
  agentName?: string;
172
180
  scopes?: string[];
package/dist/setup.js CHANGED
@@ -16,6 +16,8 @@ const DEFAULT_WAIT_INTERVAL_MS = 2_000;
16
16
  const DEFAULT_WAIT_TIMEOUT_MS = 300_000;
17
17
  const DEFAULT_MOUNT_READY_TIMEOUT_MS = 60_000;
18
18
  const DEFAULT_MOUNT_AGENT_NAME = "relayfile-mount";
19
+ const DEFAULT_MOUNT_LOCAL_LAYOUT = "exact";
20
+ const DEFAULT_MOUNT_SYNC_MODE = "mirror";
19
21
  const TOKEN_REFRESH_AGE_MS = 55 * 60 * 1000;
20
22
  const nodeOnlyMountLauncher = {
21
23
  async start() {
@@ -142,6 +144,8 @@ export class RelayfileSetup {
142
144
  localDir: normalized.localDir,
143
145
  remotePath: normalized.remotePath,
144
146
  mode: normalized.mode,
147
+ localLayout: normalized.localLayout,
148
+ syncMode: normalized.syncMode,
145
149
  background: normalized.background,
146
150
  agentName: normalized.agentName,
147
151
  scopes: normalized.scopes,
@@ -246,13 +250,18 @@ export class RelayfileSetup {
246
250
  : undefined
247
251
  });
248
252
  try {
249
- return validateMountSessionResponse(await workspace.requestJson({
253
+ const session = validateMountSessionResponse(await workspace.requestJson({
250
254
  operation: "mountWorkspace",
251
255
  method: "POST",
252
256
  path: `api/v1/workspaces/${encodeURIComponent(workspace.workspaceId)}/relayfile/mount-session`,
253
257
  body: request,
254
258
  signal: input.signal
255
259
  }), input.localDir);
260
+ return {
261
+ ...session,
262
+ localLayout: input.localLayout,
263
+ syncMode: input.syncMode
264
+ };
256
265
  }
257
266
  catch (error) {
258
267
  throw mapMountSessionError(error, request);
@@ -731,6 +740,8 @@ class MountedWorkspaceHandleImpl {
731
740
  workspaceId: this.workspaceId,
732
741
  remotePath: this.remotePath,
733
742
  mode: this.mode,
743
+ localLayout: this.mountSession.localLayout,
744
+ syncMode: this.mountSession.syncMode,
734
745
  relayfileBaseUrl: this.mountSession.relayfileBaseUrl,
735
746
  relayfileToken: this.mountSession.relayfileToken,
736
747
  expiresAt: this.expiresAt,
@@ -837,6 +848,8 @@ function validateMountSessionResponse(payload, localDir) {
837
848
  remotePath: requireStringField(payload, "remotePath"),
838
849
  localDir,
839
850
  mode: requireMountModeField(payload, "mode"),
851
+ localLayout: DEFAULT_MOUNT_LOCAL_LAYOUT,
852
+ syncMode: DEFAULT_MOUNT_SYNC_MODE,
840
853
  scopes: requireStringArrayField(payload, "scopes"),
841
854
  tokenIssuedAt: readNullableStringField(payload, "tokenIssuedAt"),
842
855
  expiresAt: readNullableStringField(payload, "expiresAt"),
@@ -930,6 +943,8 @@ function normalizeMountWorkspaceInput(input) {
930
943
  localDir: resolveLocalDir(localDir),
931
944
  remotePath: normalizeMountRemotePath(input.remotePath),
932
945
  mode: normalizeMountModeInput(input.mode),
946
+ localLayout: normalizeMountLocalLayoutInput(input.localLayout),
947
+ syncMode: normalizeMountSyncModeInput(input.syncMode),
933
948
  background: input.background !== false,
934
949
  agentName: normalizeNonEmptyString(input.agentName),
935
950
  scopes: input.scopes && input.scopes.length > 0 ? [...input.scopes] : undefined,
@@ -954,6 +969,20 @@ function normalizeMountModeInput(mode) {
954
969
  }
955
970
  return normalized;
956
971
  }
972
+ function normalizeMountLocalLayoutInput(layout) {
973
+ const normalized = normalizeNonEmptyString(layout) ?? DEFAULT_MOUNT_LOCAL_LAYOUT;
974
+ if (normalized !== "exact" && normalized !== "scoped") {
975
+ throw new MountSessionInputError(`Invalid localLayout "${normalized}" for mount session.`);
976
+ }
977
+ return normalized;
978
+ }
979
+ function normalizeMountSyncModeInput(mode) {
980
+ const normalized = normalizeNonEmptyString(mode) ?? DEFAULT_MOUNT_SYNC_MODE;
981
+ if (normalized !== "mirror" && normalized !== "write-only") {
982
+ throw new MountSessionInputError(`Invalid syncMode "${normalized}" for mount session.`);
983
+ }
984
+ return normalized;
985
+ }
957
986
  function normalizeMountRemotePath(remotePath) {
958
987
  const normalized = normalizeNonEmptyString(remotePath) ?? "/";
959
988
  if (normalized.includes("\u0000")) {
@@ -1052,6 +1081,8 @@ function buildMountedWorkspaceEnv(mountSession) {
1052
1081
  RELAYFILE_REMOTE_PATH: mountSession.remotePath,
1053
1082
  RELAYFILE_LOCAL_DIR: mountSession.localDir,
1054
1083
  RELAYFILE_MOUNT_MODE: mountSession.mode,
1084
+ RELAYFILE_MOUNT_LOCAL_LAYOUT: mountSession.localLayout,
1085
+ RELAYFILE_MOUNT_SYNC_MODE: mountSession.syncMode,
1055
1086
  RELAYCAST_API_KEY: mountSession.relaycastApiKey,
1056
1087
  RELAY_API_KEY: mountSession.relaycastApiKey,
1057
1088
  RELAYCAST_BASE_URL: relaycastBaseUrl,
package/dist/sync.d.ts CHANGED
@@ -9,6 +9,7 @@ import type { FilesystemEvent } from "./types.js";
9
9
  */
10
10
  export type RelayFileSyncTokenProvider = string | (() => string | undefined | Promise<string | undefined>);
11
11
  export type RelayFileSyncState = "idle" | "connecting" | "open" | "polling" | "reconnecting" | "closed";
12
+ export type RelayFileSyncStart = "now" | "legacy";
12
13
  export interface RelayFileSyncPong {
13
14
  type: "pong";
14
15
  timestamp?: string;
@@ -32,7 +33,9 @@ export interface RelayFileSyncOptions {
32
33
  * normally NOT pass this and let it inherit from the client.
33
34
  */
34
35
  token?: RelayFileSyncTokenProvider;
36
+ from?: RelayFileSyncStart;
35
37
  cursor?: string;
38
+ paths?: string[];
36
39
  preferPolling?: boolean;
37
40
  pollIntervalMs?: number;
38
41
  pingIntervalMs?: number;
@@ -87,6 +90,8 @@ export declare class RelayFileSync {
87
90
  private readonly handlers;
88
91
  private state;
89
92
  private cursor?;
93
+ private readonly from;
94
+ private readonly paths;
90
95
  private readonly polledEventIds;
91
96
  private readonly polledEventOrder;
92
97
  private firstPollComplete;
@@ -116,6 +121,7 @@ export declare class RelayFileSync {
116
121
  private pollLoop;
117
122
  private rememberPolledEvent;
118
123
  private handleSocketMessage;
124
+ private emitFilesystemEvent;
119
125
  private startPingLoop;
120
126
  private forceReconnect;
121
127
  private scheduleReconnect;
package/dist/sync.js CHANGED
@@ -1,3 +1,45 @@
1
+ function normalizeWebSocketPathFilters(paths) {
2
+ const seen = new Set();
3
+ const normalized = [];
4
+ for (const value of paths ?? []) {
5
+ const path = typeof value === "string" ? value.trim() : "";
6
+ if (!path) {
7
+ continue;
8
+ }
9
+ const absolute = path.startsWith("/") ? path : `/${path}`;
10
+ if (seen.has(absolute)) {
11
+ continue;
12
+ }
13
+ seen.add(absolute);
14
+ normalized.push(absolute);
15
+ }
16
+ return normalized;
17
+ }
18
+ function pathMatchesAnyFilter(filters, path) {
19
+ if (filters.length === 0) {
20
+ return true;
21
+ }
22
+ const pathSegments = normalizePathSegments(path);
23
+ return filters.some((filter) => matchPathSegments(normalizePathSegments(filter), pathSegments));
24
+ }
25
+ function normalizePathSegments(path) {
26
+ const absolute = path.startsWith("/") ? path : `/${path}`;
27
+ const trimmed = absolute.replace(/\/+$/, "");
28
+ if (!trimmed) {
29
+ return [];
30
+ }
31
+ return trimmed.split("/").filter(Boolean);
32
+ }
33
+ function matchPathSegments(pattern, path) {
34
+ if (pattern.length > 0 && pattern[pattern.length - 1] === "**") {
35
+ const prefix = pattern.slice(0, -1);
36
+ return path.length >= prefix.length && prefix.every((segment, index) => segment === "*" || segment === path[index]);
37
+ }
38
+ if (pattern.length !== path.length) {
39
+ return false;
40
+ }
41
+ return pattern.every((segment, index) => segment === "*" || segment === path[index]);
42
+ }
1
43
  const DEFAULT_POLL_INTERVAL_MS = 5000;
2
44
  const DEFAULT_PING_INTERVAL_MS = 30000;
3
45
  const DEFAULT_RECONNECT_MIN_DELAY_MS = 250;
@@ -131,6 +173,8 @@ export class RelayFileSync {
131
173
  };
132
174
  state = "idle";
133
175
  cursor;
176
+ from;
177
+ paths;
134
178
  polledEventIds = new Set();
135
179
  polledEventOrder = [];
136
180
  firstPollComplete = false;
@@ -176,7 +220,9 @@ export class RelayFileSync {
176
220
  const literal = options.token;
177
221
  this.tokenProvider = () => literal;
178
222
  }
223
+ this.from = options.from ?? "now";
179
224
  this.cursor = options.cursor;
225
+ this.paths = normalizeWebSocketPathFilters(options.paths);
180
226
  this.onPollingFallback = options.onPollingFallback;
181
227
  this.pollIntervalMs = Math.max(1, Math.floor(options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS));
182
228
  this.pingIntervalMs = Math.max(1, Math.floor(options.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS));
@@ -327,6 +373,15 @@ export class RelayFileSync {
327
373
  if (token) {
328
374
  url.searchParams.set("token", token);
329
375
  }
376
+ if (this.cursor) {
377
+ url.searchParams.set("cursor", this.cursor);
378
+ }
379
+ else if (this.from === "now") {
380
+ url.searchParams.set("from", "now");
381
+ }
382
+ for (const path of this.paths) {
383
+ url.searchParams.append("path", path);
384
+ }
330
385
  let socket;
331
386
  try {
332
387
  socket = this.webSocketFactory(url.toString());
@@ -524,7 +579,7 @@ export class RelayFileSync {
524
579
  }
525
580
  else {
526
581
  for (const event of pending) {
527
- this.emit("event", event);
582
+ this.emitFilesystemEvent(event);
528
583
  }
529
584
  }
530
585
  await this.sleep(this.pollIntervalMs);
@@ -594,8 +649,17 @@ export class RelayFileSync {
594
649
  return;
595
650
  }
596
651
  const normalized = normalizeFilesystemEvent(parsed);
652
+ if (parsed.eventId) {
653
+ this.cursor = parsed.eventId;
654
+ }
597
655
  debugLog("event", { type: normalized.type, path: normalized.path, revision: normalized.revision });
598
- this.emit("event", normalized);
656
+ this.emitFilesystemEvent(normalized);
657
+ }
658
+ emitFilesystemEvent(event) {
659
+ if (!pathMatchesAnyFilter(this.paths, event.path)) {
660
+ return;
661
+ }
662
+ this.emit("event", event);
599
663
  }
600
664
  startPingLoop(socket) {
601
665
  this.clearPingTimer();
package/dist/types.d.ts CHANGED
@@ -257,6 +257,8 @@ export interface SubscribeOptions {
257
257
  coalesce?: "none" | "fire-once";
258
258
  coalesceMs?: number;
259
259
  pathScope?: string[];
260
+ from?: "now" | "legacy";
261
+ cursor?: string;
260
262
  aclToken?: string;
261
263
  drainMs?: number;
262
264
  }
@@ -273,6 +275,8 @@ export type ReplayOptions = {
273
275
  export type ChangeStreamConnectionOptions = ReplayOptions & {
274
276
  workspaceId: string;
275
277
  aclToken?: string;
278
+ from?: "now" | "legacy";
279
+ cursor?: string;
276
280
  };
277
281
  export interface ChangeStreamConnection extends Subscription {
278
282
  readonly ready: Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relayfile/sdk",
3
- "version": "0.8.9",
3
+ "version": "0.8.11",
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",
@@ -55,7 +55,7 @@
55
55
  "prepublishOnly": "npm run build"
56
56
  },
57
57
  "dependencies": {
58
- "@relayfile/core": "0.8.9",
58
+ "@relayfile/core": "0.8.11",
59
59
  "ignore": "^7.0.5",
60
60
  "tar": "^7.5.10"
61
61
  },