mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-10 01:39:25 +00:00
refactor(agent-core-v2): switch FileStorageService.watch to chokidar
- replace node:fs fs.watch with a chokidar FSWatcher on the parent directory (depth 0), filtering events by normalized path so the match is correct on Windows as well as POSIX - keep the existing Event<void> contract, 150ms debounce, and ref-counted arm/disarm lifecycle - add chokidar ^4.0.3 and refresh the lockfile - settle the watcher in storage watch tests to account for chokidar's asynchronous OS-watcher attachment
This commit is contained in:
parent
b94c60d50a
commit
78fa333c98
4 changed files with 33 additions and 12 deletions
|
|
@ -62,6 +62,7 @@
|
|||
"@moonshot-ai/kimi-telemetry": "workspace:^",
|
||||
"@moonshot-ai/kosong": "workspace:^",
|
||||
"@moonshot-ai/protocol": "workspace:^",
|
||||
"chokidar": "^4.0.3",
|
||||
"ignore": "^5.3.2",
|
||||
"js-yaml": "^4.1.1",
|
||||
"nunjucks": "^3.2.4",
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@
|
|||
* fsync, so the replacement is both atomic and durable.
|
||||
* - `append` → `open('a')` + write + `fh.sync()` (when `durable`), plus a
|
||||
* one-time directory fsync per scope.
|
||||
* - `watch` → `fs.watch` the parent directory (filtered by filename and
|
||||
* debounced), so it survives atomic-replace renames and observes
|
||||
* a file that does not exist yet.
|
||||
* - `watch` → chokidar on the parent directory, filtered to the exact key and
|
||||
* debounced, so it survives atomic-replace renames and observes a
|
||||
* file that does not exist yet at subscription time.
|
||||
*
|
||||
* It uses raw `node:fs` rather than `kaos`: the storage kernel needs direct
|
||||
* control over append offsets, fsync, atomic rename and streaming, which the
|
||||
|
|
@ -21,9 +21,10 @@
|
|||
* this backend, never `node:fs` directly.
|
||||
*/
|
||||
|
||||
import { createReadStream, mkdirSync, watch as fsWatch, type FSWatcher } from 'node:fs';
|
||||
import { createReadStream, mkdirSync } from 'node:fs';
|
||||
import { mkdir, open, readFile, readdir, unlink } from 'node:fs/promises';
|
||||
import { basename, dirname, join } from 'pathe';
|
||||
import { FSWatcher } from 'chokidar';
|
||||
import { dirname, join, normalize } from 'pathe';
|
||||
|
||||
import {
|
||||
DisposableStore,
|
||||
|
|
@ -137,7 +138,7 @@ export class FileStorageService implements IStorageService {
|
|||
watch(scope: string, key: string): Event<void> {
|
||||
const target = this.path(scope, key);
|
||||
const dir = dirname(target);
|
||||
const name = basename(target);
|
||||
const normalizedTarget = normalize(target);
|
||||
const emitter = new Emitter<void>();
|
||||
|
||||
let watcher: FSWatcher | undefined;
|
||||
|
|
@ -149,16 +150,24 @@ export class FileStorageService implements IStorageService {
|
|||
timer = setTimeout(() => emitter.fire(), WATCH_DEBOUNCE_MS);
|
||||
};
|
||||
|
||||
// Watch the parent directory and filter by filename: the directory survives
|
||||
// atomic-replace renames (which would detach an inode watcher), and it lets
|
||||
// us observe a file that does not exist yet at subscription time.
|
||||
// Watch the parent directory and filter by exact path: the directory survives
|
||||
// atomic-replace renames (which would detach a single-file watcher) and it
|
||||
// lets us observe a file that does not exist yet at subscription time. Events
|
||||
// are debounced to collapse the burst a single save (plus its atomic-replace
|
||||
// temp file) emits.
|
||||
const arm = (): void => {
|
||||
try {
|
||||
mkdirSync(dir, { recursive: true, mode: this.dirMode });
|
||||
watcher = fsWatch(dir, (_event, filename) => {
|
||||
if (filename === null || filename === name) schedule();
|
||||
watcher = new FSWatcher({
|
||||
ignoreInitial: true,
|
||||
awaitWriteFinish: false,
|
||||
depth: 0,
|
||||
});
|
||||
watcher.on('all', (_event, changedPath) => {
|
||||
if (normalize(changedPath) === normalizedTarget) schedule();
|
||||
});
|
||||
watcher.on('error', () => undefined);
|
||||
watcher.add(dir);
|
||||
} catch {
|
||||
// Best effort: callers can still reload explicitly when watching fails.
|
||||
}
|
||||
|
|
@ -169,7 +178,8 @@ export class FileStorageService implements IStorageService {
|
|||
clearTimeout(timer);
|
||||
timer = undefined;
|
||||
}
|
||||
watcher?.close();
|
||||
const closeResult = watcher?.close();
|
||||
if (closeResult !== undefined) void closeResult.catch(() => undefined);
|
||||
watcher = undefined;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ function storageServiceSuite(
|
|||
await cleanup?.();
|
||||
});
|
||||
|
||||
// chokidar-backed storage attaches the OS watcher asynchronously; give it a
|
||||
// moment to register before mutating. Harmless for the in-memory backend.
|
||||
const settle = (): Promise<void> =>
|
||||
new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
it('read returns undefined for a missing key', async () => {
|
||||
expect(await service.read('s', 'missing')).toBeUndefined();
|
||||
});
|
||||
|
|
@ -94,6 +99,7 @@ function storageServiceSuite(
|
|||
resolve();
|
||||
});
|
||||
});
|
||||
await settle();
|
||||
await service.write('s', 'k', enc.encode('v'));
|
||||
await fired;
|
||||
});
|
||||
|
|
@ -119,6 +125,7 @@ function storageServiceSuite(
|
|||
resolve();
|
||||
});
|
||||
});
|
||||
await settle();
|
||||
await service.delete('s', 'k');
|
||||
await fired;
|
||||
});
|
||||
|
|
|
|||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
|
|
@ -453,6 +453,9 @@ importers:
|
|||
'@moonshot-ai/protocol':
|
||||
specifier: workspace:^
|
||||
version: link:../protocol
|
||||
chokidar:
|
||||
specifier: ^4.0.3
|
||||
version: 4.0.3
|
||||
ignore:
|
||||
specifier: ^5.3.2
|
||||
version: 5.3.2
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue