@relayfile/local-mount 0.7.17 → 0.7.19
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/README.md +9 -3
- package/dist/auto-sync.d.ts +10 -1
- package/dist/auto-sync.js +59 -11
- package/dist/mount.d.ts +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,8 +74,10 @@ By default, `launchOnMount` keeps the mount and project directory in sync contin
|
|
|
74
74
|
|
|
75
75
|
```ts
|
|
76
76
|
interface AutoSyncOptions {
|
|
77
|
-
/**
|
|
77
|
+
/** Degraded-watcher full-reconcile interval. Default: 10_000 ms. 0/Infinity disables periodic scans. */
|
|
78
78
|
scanIntervalMs?: number;
|
|
79
|
+
/** Healthy-watcher full-reconcile interval. Default: 60_000 ms, or scanIntervalMs when set. */
|
|
80
|
+
healthyScanIntervalMs?: number;
|
|
79
81
|
/** Per-path event debounce in ms. Default: 50 ms. */
|
|
80
82
|
debounceMs?: number;
|
|
81
83
|
/** Invoked on sync errors. Defaults to swallowing them. */
|
|
@@ -100,12 +102,16 @@ Control it from `launchOnMount`:
|
|
|
100
102
|
launchOnMount({ /* ... */, autoSync: false });
|
|
101
103
|
|
|
102
104
|
// Tune it.
|
|
103
|
-
launchOnMount({ /* ... */, autoSync: {
|
|
105
|
+
launchOnMount({ /* ... */, autoSync: { healthyScanIntervalMs: 120_000, debounceMs: 100 } });
|
|
106
|
+
|
|
107
|
+
// Disable periodic full reconciles and rely on watcher events.
|
|
108
|
+
launchOnMount({ /* ... */, autoSync: { scanIntervalMs: 0 } });
|
|
104
109
|
```
|
|
105
110
|
|
|
106
111
|
How it works:
|
|
107
112
|
- [@parcel/watcher](https://www.npmjs.com/package/@parcel/watcher) watches both the mount and the project tree using native FSEvents/inotify/ReadDirectoryChangesW
|
|
108
|
-
- every `
|
|
113
|
+
- every `healthyScanIntervalMs` while watchers are healthy, a full reconcile walks both trees as a low-frequency safety net for missed events
|
|
114
|
+
- if watcher setup fails or a watcher reports an error, full reconciles fall back to `scanIntervalMs`
|
|
109
115
|
- watcher events are tracked as dirty paths, so shutdown can flush pending path-level work and make the final sync-back proportional to the number of mount-side changes when the watcher state stayed healthy
|
|
110
116
|
- `stop({ signal })` still closes watchers if aborted, but skips the final draining reconcile
|
|
111
117
|
- per-file `mtime` is tracked at the last sync, so the scan skips files that haven't changed
|
package/dist/auto-sync.d.ts
CHANGED
|
@@ -32,8 +32,17 @@ export interface AutoSyncContext {
|
|
|
32
32
|
isReservedFile: (relPosix: string) => boolean;
|
|
33
33
|
}
|
|
34
34
|
export interface AutoSyncOptions {
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Degraded-watcher full-reconcile interval as a safety net. Default: 10_000ms.
|
|
37
|
+
* Set to 0 or Infinity to disable periodic full reconciles.
|
|
38
|
+
*/
|
|
36
39
|
scanIntervalMs?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Full-reconcile interval while both watcher subscriptions are healthy.
|
|
42
|
+
* Default: 60_000ms, or `scanIntervalMs` when that option is explicitly set.
|
|
43
|
+
* Set to 0 or Infinity to disable healthy-watcher full reconciles.
|
|
44
|
+
*/
|
|
45
|
+
healthyScanIntervalMs?: number;
|
|
37
46
|
/**
|
|
38
47
|
* Per-path event debounce in ms. Rapid watcher events for the same path
|
|
39
48
|
* are coalesced into a single sync. Default: 50.
|
package/dist/auto-sync.js
CHANGED
|
@@ -2,8 +2,21 @@ import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync,
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import watcher from '@parcel/watcher';
|
|
4
4
|
const STOP_EVENT_SETTLE_MS = 250;
|
|
5
|
+
const DEFAULT_SCAN_INTERVAL_MS = 10_000;
|
|
6
|
+
const DEFAULT_HEALTHY_SCAN_INTERVAL_MS = 60_000;
|
|
7
|
+
const MAX_SCAN_INTERVAL_MS = 2_147_483_647;
|
|
8
|
+
function normalizeScanInterval(name, value, fallback) {
|
|
9
|
+
const interval = value ?? fallback;
|
|
10
|
+
if (interval === 0 || interval === Infinity)
|
|
11
|
+
return null;
|
|
12
|
+
if (!Number.isFinite(interval) || interval < 0 || interval > MAX_SCAN_INTERVAL_MS) {
|
|
13
|
+
throw new RangeError(`${name} must be between 0 and ${MAX_SCAN_INTERVAL_MS}, or Infinity`);
|
|
14
|
+
}
|
|
15
|
+
return interval;
|
|
16
|
+
}
|
|
5
17
|
export function startAutoSync(ctx, opts = {}) {
|
|
6
|
-
const scanIntervalMs = opts.scanIntervalMs
|
|
18
|
+
const scanIntervalMs = normalizeScanInterval('scanIntervalMs', opts.scanIntervalMs, DEFAULT_SCAN_INTERVAL_MS);
|
|
19
|
+
const healthyScanIntervalMs = normalizeScanInterval('healthyScanIntervalMs', opts.healthyScanIntervalMs, opts.scanIntervalMs === undefined ? DEFAULT_HEALTHY_SCAN_INTERVAL_MS : opts.scanIntervalMs);
|
|
7
20
|
const debounceMs = opts.debounceMs ?? 50;
|
|
8
21
|
const onError = opts.onError ?? (() => { });
|
|
9
22
|
const state = new Map();
|
|
@@ -19,10 +32,6 @@ export function startAutoSync(ctx, opts = {}) {
|
|
|
19
32
|
const pendingDebounces = new Map();
|
|
20
33
|
const dirtyMountPaths = new Set();
|
|
21
34
|
const watchersHealthy = () => watchersReadySettled && !watcherDegraded;
|
|
22
|
-
const markWatcherDegraded = (err) => {
|
|
23
|
-
watcherDegraded = true;
|
|
24
|
-
onError(err);
|
|
25
|
-
};
|
|
26
35
|
const clearPendingDebounces = () => {
|
|
27
36
|
for (const t of pendingDebounces.values())
|
|
28
37
|
clearTimeout(t);
|
|
@@ -104,6 +113,47 @@ export function startAutoSync(ctx, opts = {}) {
|
|
|
104
113
|
totalChanges += count;
|
|
105
114
|
return count;
|
|
106
115
|
};
|
|
116
|
+
let periodicTimer;
|
|
117
|
+
let periodicReconcileRunning = false;
|
|
118
|
+
const clearPeriodicReconcile = () => {
|
|
119
|
+
if (periodicTimer) {
|
|
120
|
+
clearTimeout(periodicTimer);
|
|
121
|
+
periodicTimer = undefined;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const nextScanInterval = () => {
|
|
125
|
+
return watchersHealthy() ? healthyScanIntervalMs : scanIntervalMs;
|
|
126
|
+
};
|
|
127
|
+
const schedulePeriodicReconcile = () => {
|
|
128
|
+
clearPeriodicReconcile();
|
|
129
|
+
if (stopping || stopped)
|
|
130
|
+
return;
|
|
131
|
+
const delay = nextScanInterval();
|
|
132
|
+
if (delay === null)
|
|
133
|
+
return;
|
|
134
|
+
periodicTimer = setTimeout(() => {
|
|
135
|
+
periodicTimer = undefined;
|
|
136
|
+
periodicReconcileRunning = true;
|
|
137
|
+
void runReconcile().finally(() => {
|
|
138
|
+
periodicReconcileRunning = false;
|
|
139
|
+
schedulePeriodicReconcile();
|
|
140
|
+
});
|
|
141
|
+
}, delay);
|
|
142
|
+
periodicTimer.unref?.();
|
|
143
|
+
};
|
|
144
|
+
const reschedulePeriodicReconcile = () => {
|
|
145
|
+
if (periodicReconcileRunning)
|
|
146
|
+
return;
|
|
147
|
+
schedulePeriodicReconcile();
|
|
148
|
+
};
|
|
149
|
+
const markWatcherDegraded = (err) => {
|
|
150
|
+
const alreadyDegraded = watcherDegraded;
|
|
151
|
+
watcherDegraded = true;
|
|
152
|
+
if (!alreadyDegraded) {
|
|
153
|
+
reschedulePeriodicReconcile();
|
|
154
|
+
}
|
|
155
|
+
onError(err);
|
|
156
|
+
};
|
|
107
157
|
const flushPending = async (opts) => {
|
|
108
158
|
if (opts?.signal?.aborted) {
|
|
109
159
|
return 0;
|
|
@@ -178,10 +228,12 @@ export function startAutoSync(ctx, opts = {}) {
|
|
|
178
228
|
projectSub = projectResult.value;
|
|
179
229
|
if (mountResult.status === 'fulfilled' && projectResult.status === 'fulfilled') {
|
|
180
230
|
watchersReadySettled = true;
|
|
231
|
+
reschedulePeriodicReconcile();
|
|
181
232
|
return;
|
|
182
233
|
}
|
|
183
234
|
watchersReadySettled = true;
|
|
184
235
|
watcherDegraded = true;
|
|
236
|
+
reschedulePeriodicReconcile();
|
|
185
237
|
await Promise.allSettled([
|
|
186
238
|
mountSub?.unsubscribe(),
|
|
187
239
|
projectSub?.unsubscribe(),
|
|
@@ -196,11 +248,7 @@ export function startAutoSync(ctx, opts = {}) {
|
|
|
196
248
|
// rejection. stop() still awaits the same promise and will observe the
|
|
197
249
|
// rejection after the cleanup above has already run.
|
|
198
250
|
watchersReady.catch((err) => markWatcherDegraded(err));
|
|
199
|
-
|
|
200
|
-
void runReconcile();
|
|
201
|
-
}, scanIntervalMs);
|
|
202
|
-
// Do not keep the event loop alive just because of our scan timer.
|
|
203
|
-
interval.unref?.();
|
|
251
|
+
schedulePeriodicReconcile();
|
|
204
252
|
return {
|
|
205
253
|
async stop(opts) {
|
|
206
254
|
try {
|
|
@@ -214,7 +262,7 @@ export function startAutoSync(ctx, opts = {}) {
|
|
|
214
262
|
await new Promise((resolve) => setTimeout(resolve, STOP_EVENT_SETTLE_MS));
|
|
215
263
|
}
|
|
216
264
|
stopping = true;
|
|
217
|
-
|
|
265
|
+
clearPeriodicReconcile();
|
|
218
266
|
clearPendingDebounces();
|
|
219
267
|
await Promise.allSettled([
|
|
220
268
|
mountSub?.unsubscribe(),
|
package/dist/mount.d.ts
CHANGED
|
@@ -36,8 +36,9 @@ export interface MountHandle {
|
|
|
36
36
|
}): Promise<number>;
|
|
37
37
|
/**
|
|
38
38
|
* Start bidirectional auto-sync: watches both the mount and project trees
|
|
39
|
-
* via @parcel/watcher and runs
|
|
40
|
-
*
|
|
39
|
+
* via @parcel/watcher and runs periodic full reconciles as a safety net,
|
|
40
|
+
* with a slower cadence while watchers are healthy. Returns a handle you
|
|
41
|
+
* must `stop()` before teardown.
|
|
41
42
|
*/
|
|
42
43
|
startAutoSync(opts?: AutoSyncOptions): AutoSyncHandle;
|
|
43
44
|
cleanup(): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relayfile/local-mount",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.19",
|
|
4
4
|
"description": "Create a symlink/copy mount of a project directory with .agentignore/.agentreadonly semantics, then launch a CLI inside it and sync writable changes back on exit",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|