@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 +3 -0
- package/dist/client.js +67 -6
- package/dist/index.d.ts +2 -2
- package/dist/mount-launcher.js +33 -4
- package/dist/setup-types.d.ts +8 -0
- package/dist/setup.js +32 -1
- package/dist/sync.d.ts +6 -0
- package/dist/sync.js +66 -2
- package/dist/types.d.ts +4 -0
- package/package.json +2 -2
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";
|
package/dist/mount-launcher.js
CHANGED
|
@@ -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
|
|
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 ??
|
|
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;
|
package/dist/setup-types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
58
|
+
"@relayfile/core": "0.8.11",
|
|
59
59
|
"ignore": "^7.0.5",
|
|
60
60
|
"tar": "^7.5.10"
|
|
61
61
|
},
|