@vzn/vx 0.0.435 → 0.0.437
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/package.json +5 -5
- package/src/cli/watch-fs.ts +100 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.437",
|
|
4
4
|
"description": "The fastest task runner and build cache for JavaScript monorepos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"node": ">=18"
|
|
22
22
|
},
|
|
23
23
|
"optionalDependencies": {
|
|
24
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
25
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
26
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
27
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
24
|
+
"@vzn/vx-linux-x64": "0.0.437",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.437",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.437",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.437"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cli/watch-fs.ts
CHANGED
|
@@ -26,7 +26,7 @@ export interface WatchHandle {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export interface ArmedWatcher {
|
|
29
|
-
watcher:
|
|
29
|
+
watcher: WatchHandle
|
|
30
30
|
/** Resolves `true` once the watcher reported the probe, `false` on timeout. */
|
|
31
31
|
ready: Promise<boolean>
|
|
32
32
|
}
|
|
@@ -157,7 +157,7 @@ export function armWatcher(
|
|
|
157
157
|
const seen = new Promise<boolean>((resolve) => {
|
|
158
158
|
markReady = resolve
|
|
159
159
|
})
|
|
160
|
-
const
|
|
160
|
+
const listener = (filename: string | null): void => {
|
|
161
161
|
if (filename == null || typeof filename !== 'string') return
|
|
162
162
|
// An event naming the watched directory itself (macOS reports the
|
|
163
163
|
// directory a write landed in as its own item) carries nothing a key
|
|
@@ -175,7 +175,11 @@ export function armWatcher(
|
|
|
175
175
|
// proves this stream live.
|
|
176
176
|
if (path.basename(filename) === WATCH_PROBE) return
|
|
177
177
|
onEvent(filename)
|
|
178
|
-
}
|
|
178
|
+
}
|
|
179
|
+
const watcher: WatchHandle =
|
|
180
|
+
recursive && process.platform === 'linux'
|
|
181
|
+
? treeWatcher(dir, listener)
|
|
182
|
+
: fs.watch(dir, { recursive, persistent: true }, (_event, filename) => listener(filename))
|
|
179
183
|
const probe = path.join(dir, WATCH_PROBE)
|
|
180
184
|
const ready = (async (): Promise<boolean> => {
|
|
181
185
|
// The probe is subject to the very race it detects: a write that lands
|
|
@@ -217,6 +221,99 @@ export function armWatcher(
|
|
|
217
221
|
return { watcher, ready }
|
|
218
222
|
}
|
|
219
223
|
|
|
224
|
+
/**
|
|
225
|
+
* A recursive watch on Linux, built from one non-recursive watch per
|
|
226
|
+
* directory, that never enters `IGNORED_SEGMENTS`. Bun's recursive form is
|
|
227
|
+
* the same inotify watch per directory and descended into all of them:
|
|
228
|
+
* this repo's root arm held 4,657 watches where 438 directories can
|
|
229
|
+
* matter, 40–55 ms of the arm, and a monorepo's `node_modules` met the OS
|
|
230
|
+
* watch limit (8,192 on many distros) and fell back to polling — for
|
|
231
|
+
* events the loop drops by name. A directory that appears is watched, and
|
|
232
|
+
* what it already holds is reported (it may have landed before its watch);
|
|
233
|
+
* one that goes, or moves, is dropped with everything under it, since an
|
|
234
|
+
* inotify watch follows the inode and would report the old name. The
|
|
235
|
+
* arming walk throws at the watch limit, so the pool's fallback still
|
|
236
|
+
* applies; a subdirectory that vanished mid-walk is simply not watched.
|
|
237
|
+
*/
|
|
238
|
+
function treeWatcher(root: string, listener: (filename: string) => void): WatchHandle {
|
|
239
|
+
const watchers = new Map<string, fs.FSWatcher>()
|
|
240
|
+
let closed = false
|
|
241
|
+
let armed = false
|
|
242
|
+
let warned = false
|
|
243
|
+
const drop = (rel: string): void => {
|
|
244
|
+
for (const [key, w] of watchers) {
|
|
245
|
+
if (key !== rel && !key.startsWith(rel + '/')) continue
|
|
246
|
+
w.close()
|
|
247
|
+
watchers.delete(key)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const watchDir = (rel: string, report: boolean): void => {
|
|
251
|
+
if (closed || watchers.has(rel)) return
|
|
252
|
+
const abs = rel === '' ? root : path.join(root, rel)
|
|
253
|
+
let w: fs.FSWatcher
|
|
254
|
+
try {
|
|
255
|
+
w = fs.watch(abs, { persistent: true }, (_event, name) => {
|
|
256
|
+
if (name == null || typeof name !== 'string' || name === '' || name === '.') return
|
|
257
|
+
const child = rel === '' ? name : `${rel}/${name}`
|
|
258
|
+
listener(child)
|
|
259
|
+
if (IGNORED_SEGMENTS.includes(name)) return
|
|
260
|
+
let st: fs.Stats
|
|
261
|
+
try {
|
|
262
|
+
st = fs.statSync(path.join(abs, name))
|
|
263
|
+
} catch {
|
|
264
|
+
drop(child)
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
if (!st.isDirectory()) return
|
|
268
|
+
if (watchers.has(child)) return
|
|
269
|
+
watchDir(child, true)
|
|
270
|
+
})
|
|
271
|
+
} catch (err) {
|
|
272
|
+
const code = (err as NodeJS.ErrnoException).code
|
|
273
|
+
const limit = code === 'ENOSPC' || code === 'EMFILE'
|
|
274
|
+
// The walk that arms is all or nothing at the watch limit, as the
|
|
275
|
+
// recursive form was, so the pool's fallback polls instead of a
|
|
276
|
+
// tree half watched without a word.
|
|
277
|
+
if (rel === '' || (limit && !armed)) throw err
|
|
278
|
+
if (limit && !warned) {
|
|
279
|
+
warned = true
|
|
280
|
+
process.stderr.write(
|
|
281
|
+
`vx watch: ${abs}: the OS watch limit is reached (${code}); directories made from here on are not watched — raise it (Linux: sysctl fs.inotify.max_user_watches)\n`,
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
w.on('error', () => drop(rel))
|
|
287
|
+
watchers.set(rel, w)
|
|
288
|
+
let entries: fs.Dirent[]
|
|
289
|
+
try {
|
|
290
|
+
entries = fs.readdirSync(abs, { withFileTypes: true })
|
|
291
|
+
} catch {
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
for (const e of entries) {
|
|
295
|
+
const child = rel === '' ? e.name : `${rel}/${e.name}`
|
|
296
|
+
if (e.isDirectory()) {
|
|
297
|
+
if (!IGNORED_SEGMENTS.includes(e.name)) watchDir(child, report)
|
|
298
|
+
} else if (report) listener(child)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
watchDir('', false)
|
|
303
|
+
} catch (err) {
|
|
304
|
+
for (const w of watchers.values()) w.close()
|
|
305
|
+
throw err
|
|
306
|
+
}
|
|
307
|
+
armed = true
|
|
308
|
+
return {
|
|
309
|
+
close(): void {
|
|
310
|
+
closed = true
|
|
311
|
+
for (const w of watchers.values()) w.close()
|
|
312
|
+
watchers.clear()
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
220
317
|
/** True when `abs` was last modified before `t` (epoch ms); false when it cannot be read. */
|
|
221
318
|
export function modifiedBefore(abs: string, t: number): boolean {
|
|
222
319
|
try {
|