@relayfile/sdk 0.8.8 → 0.8.10

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
@@ -3,7 +3,7 @@ export { RelayfileSetup, RELAYFILE_SDK_VERSION, WorkspaceHandle } from "./setup.
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
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";
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";
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.8",
3
+ "version": "0.8.10",
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.8",
58
+ "@relayfile/core": "0.8.10",
59
59
  "ignore": "^7.0.5",
60
60
  "tar": "^7.5.10"
61
61
  },