← Sandbox SDK / sandbox / guides
Отслеживание изменений файловой системы
В этом руководстве показано, как отслеживать изменения файловой системы в реальном времени с помощью API отслеживания файлов Sandbox SDK. Отслеживание файлов полезно при создании инструментов разработки, автоматизированных рабочих процессов и приложений, реагирующих на изменения файлов по мере их появления.
watch() метод возвращает поток SSE (Server-Sent Events), который вы обрабатываете с помощью parseSSEStream(). Каждое событие в потоке описывает изменение файловой системы.
Базовое отслеживание файлов
Начните с наблюдения за директорией на предмет изменений:
import { parseSSEStream } from "@cloudflare/sandbox";
const stream = await sandbox.watch("/workspace/src");
for await (const event of parseSSEStream(stream)) {
if (event.type === "event") {
console.log(`${event.eventType}: ${event.path}`);
console.log(`Is directory: ${event.isDirectory}`);
}
}import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";
const stream = await sandbox.watch("/workspace/src");
for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
if (event.type === "event") {
console.log(`${event.eventType}: ${event.path}`);
console.log(`Is directory: ${event.isDirectory}`);
}
}Поток генерирует четыре типа событий жизненного цикла:
watching: Отслеживание установлено, включаетwatchIdevent: произошло изменение в файловой системеerror: Отслеживание завершилось ошибкойstopped: Отслеживание было остановлено
События изменения файловой системы (event.eventType) включают:
create: файл или каталог созданmodify: изменено содержимое файлаdelete: файл или каталог удалёнmove_from/move_to: файл или каталог перемещён или переименованattrib: изменены атрибуты файла (права доступа, временные метки)
Фильтрация по типу файла
Используйте include шаблоны, чтобы отслеживать только определённые типы файлов:
import { parseSSEStream } from "@cloudflare/sandbox";
// Only watch TypeScript and JavaScript files
const stream = await sandbox.watch("/workspace/src", {
include: ["*.ts", "*.tsx", "*.js", "*.jsx"],
});
for await (const event of parseSSEStream(stream)) {
if (event.type === "event") {
console.log(`${event.eventType}: ${event.path}`);
}
}import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";
// Only watch TypeScript and JavaScript files
const stream = await sandbox.watch("/workspace/src", {
include: ["*.ts", "*.tsx", "*.js", "*.jsx"],
});
for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
if (event.type === "event") {
console.log(`${event.eventType}: ${event.path}`);
}
}Частые шаблоны включения:
*.ts: Файлы TypeScript*.js: файлы JavaScript*.json: конфигурационные файлы JSON*.md: документация в формате Markdownpackage*.json: в частности, файлы пакетов
Исключение каталогов
Используйте exclude шаблоны, чтобы пропустить определённые каталоги или файлы:
import { parseSSEStream } from "@cloudflare/sandbox";
const stream = await sandbox.watch("/workspace", {
exclude: ["node_modules", "dist", "*.log", ".git", "*.tmp"],
});
for await (const event of parseSSEStream(stream)) {
if (event.type === "event") {
console.log(`Change detected: ${event.path}`);
}
}import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";
const stream = await sandbox.watch("/workspace", {
exclude: ["node_modules", "dist", "*.log", ".git", "*.tmp"],
});
for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
if (event.type === "event") {
console.log(`Change detected: ${event.path}`);
}
}Создание отзывчивых инструментов разработки
Автоматическая пересборка при изменениях
Запускайте сборки автоматически при изменении исходных файлов:
import { parseSSEStream } from "@cloudflare/sandbox";
const stream = await sandbox.watch("/workspace/src", {
include: ["*.ts", "*.tsx"],
});
let buildInProgress = false;
for await (const event of parseSSEStream(stream)) {
if (
event.type === "event" &&
event.eventType === "modify" &&
!buildInProgress
) {
buildInProgress = true;
console.log(`File changed: ${event.path}, rebuilding...`);
try {
const result = await sandbox.exec("npm run build");
if (result.success) {
console.log("Build completed successfully");
} else {
console.error("Build failed:", result.stderr);
}
} catch (error) {
console.error("Build error:", error);
} finally {
buildInProgress = false;
}
}
}import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";
const stream = await sandbox.watch("/workspace/src", {
include: ["*.ts", "*.tsx"],
});
let buildInProgress = false;
for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
if (
event.type === "event" &&
event.eventType === "modify" &&
!buildInProgress
) {
buildInProgress = true;
console.log(`File changed: ${event.path}, rebuilding...`);
try {
const result = await sandbox.exec("npm run build");
if (result.success) {
console.log("Build completed successfully");
} else {
console.error("Build failed:", result.stderr);
}
} catch (error) {
console.error("Build error:", error);
} finally {
buildInProgress = false;
}
}
}Автоматический запуск тестов при изменениях
Повторно запускайте тесты при изменении файлов тестов:
import { parseSSEStream } from "@cloudflare/sandbox";
const stream = await sandbox.watch("/workspace/tests", {
include: ["*.test.ts", "*.spec.ts"],
});
for await (const event of parseSSEStream(stream)) {
if (event.type === "event" && event.eventType === "modify") {
console.log(`Test file changed: ${event.path}`);
const result = await sandbox.exec(`npm test -- ${event.path}`);
console.log(result.success ? "Tests passed" : "Tests failed");
}
}import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";
const stream = await sandbox.watch("/workspace/tests", {
include: ["*.test.ts", "*.spec.ts"],
});
for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
if (event.type === "event" && event.eventType === "modify") {
console.log(`Test file changed: ${event.path}`);
const result = await sandbox.exec(`npm test -- ${event.path}`);
console.log(result.success ? "Tests passed" : "Tests failed");
}
}Инкрементальная индексация
Переиндексируйте только изменённые файлы вместо повторного сканирования всего дерева каталогов:
import { parseSSEStream } from "@cloudflare/sandbox";
const stream = await sandbox.watch("/workspace/docs", {
include: ["*.md", "*.mdx"],
});
for await (const event of parseSSEStream(stream)) {
if (event.type === "event") {
switch (event.eventType) {
case "create":
case "modify":
console.log(`Indexing ${event.path}...`);
await indexFile(event.path);
break;
case "delete":
console.log(`Removing ${event.path} from index...`);
await removeFromIndex(event.path);
break;
}
}
}import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";
const stream = await sandbox.watch("/workspace/docs", {
include: ["*.md", "*.mdx"],
});
for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
if (event.type === "event") {
switch (event.eventType) {
case "create":
case "modify":
console.log(`Indexing ${event.path}...`);
await indexFile(event.path);
break;
case "delete":
console.log(`Removing ${event.path} from index...`);
await removeFromIndex(event.path);
break;
}
}
}Расширенные паттерны
События процесса с помощью вспомогательной функции
Вынесите обработку событий в переиспользуемую функцию, которая управляет жизненным циклом потока:
import { parseSSEStream } from "@cloudflare/sandbox";
async function watchFiles(sandbox, path, options, handler) {
const stream = await sandbox.watch(path, options);
for await (const event of parseSSEStream(stream)) {
switch (event.type) {
case "watching":
console.log(`Watching ${event.path}`);
break;
case "event":
await handler(event.eventType, event.path, event.isDirectory);
break;
case "error":
console.error(`Watch error: ${event.error}`);
break;
case "stopped":
console.log(`Watch stopped: ${event.reason}`);
return;
}
}
}
// Usage
await watchFiles(
sandbox,
"/workspace/src",
{ include: ["*.ts"] },
async (eventType, filePath) => {
console.log(`${eventType}: ${filePath}`);
},
);import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";
async function watchFiles(
sandbox: any,
path: string,
options: { include?: string[]; exclude?: string[] },
handler: (
eventType: string,
filePath: string,
isDirectory: boolean,
) => Promise<void>,
) {
const stream = await sandbox.watch(path, options);
for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
switch (event.type) {
case "watching":
console.log(`Watching ${event.path}`);
break;
case "event":
await handler(event.eventType, event.path, event.isDirectory);
break;
case "error":
console.error(`Watch error: ${event.error}`);
break;
case "stopped":
console.log(`Watch stopped: ${event.reason}`);
return;
}
}
}
// Usage
await watchFiles(
sandbox,
"/workspace/src",
{ include: ["*.ts"] },
async (eventType, filePath) => {
console.log(`${eventType}: ${filePath}`);
},
);Файловые операции с debounce
Чтобы избежать избыточных операций, собирайте изменения перед обработкой:
import { parseSSEStream } from "@cloudflare/sandbox";
const stream = await sandbox.watch("/workspace/src");
const changedFiles = new Set();
let debounceTimeout = null;
for await (const event of parseSSEStream(stream)) {
if (event.type === "event") {
changedFiles.add(event.path);
if (debounceTimeout) {
clearTimeout(debounceTimeout);
}
debounceTimeout = setTimeout(async () => {
console.log(`Processing ${changedFiles.size} changed files...`);
for (const filePath of changedFiles) {
await processFile(filePath);
}
changedFiles.clear();
debounceTimeout = null;
}, 1000);
}
}import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";
const stream = await sandbox.watch("/workspace/src");
const changedFiles = new Set<string>();
let debounceTimeout: ReturnType<typeof setTimeout> | null = null;
for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
if (event.type === "event") {
changedFiles.add(event.path);
if (debounceTimeout) {
clearTimeout(debounceTimeout);
}
debounceTimeout = setTimeout(async () => {
console.log(`Processing ${changedFiles.size} changed files...`);
for (const filePath of changedFiles) {
await processFile(filePath);
}
changedFiles.clear();
debounceTimeout = null;
}, 1000);
}
}Отслеживание в нерекурсивном режиме
Отслеживайте только верхний уровень директории, не спускаясь во вложенные папки:
import { parseSSEStream } from "@cloudflare/sandbox";
// Only watch root-level config files
const stream = await sandbox.watch("/workspace", {
include: ["package.json", "tsconfig.json", "vite.config.ts"],
recursive: false,
});
for await (const event of parseSSEStream(stream)) {
if (event.type === "event") {
console.log("Configuration changed, rebuilding project...");
await sandbox.exec("npm run build");
}
}import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";
// Only watch root-level config files
const stream = await sandbox.watch("/workspace", {
include: ["package.json", "tsconfig.json", "vite.config.ts"],
recursive: false,
});
for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
if (event.type === "event") {
console.log("Configuration changed, rebuilding project...");
await sandbox.exec("npm run build");
}
}Остановите наблюдение
Поток естественным образом завершается, когда контейнер переходит в спящий режим или останавливается. Есть два способа остановить наблюдение досрочно:
Используйте AbortController
Передайте AbortSignal к parseSSEStream. Прерывание сигнала отменяет чтение потока, что распространяет очистку на сервер. Это рекомендуемый подход, когда нужно отменить наблюдение извне цикла обработки:
import { parseSSEStream } from "@cloudflare/sandbox";
const stream = await sandbox.watch("/workspace/src");
const controller = new AbortController();
// Cancel after 60 seconds
setTimeout(() => controller.abort(), 60_000);
for await (const event of parseSSEStream(stream, controller.signal)) {
if (event.type === "event") {
console.log(`${event.eventType}: ${event.path}`);
}
}
console.log("Watch stopped");import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";
const stream = await sandbox.watch("/workspace/src");
const controller = new AbortController();
// Cancel after 60 seconds
setTimeout(() => controller.abort(), 60_000);
for await (const event of parseSSEStream<FileWatchSSEEvent>(
stream,
controller.signal,
)) {
if (event.type === "event") {
console.log(`${event.eventType}: ${event.path}`);
}
}
console.log("Watch stopped");Выйдите из цикла
Выход из for await цикл также отменяет поток:
import { parseSSEStream } from "@cloudflare/sandbox";
const stream = await sandbox.watch("/workspace/src");
let eventCount = 0;
for await (const event of parseSSEStream(stream)) {
if (event.type === "event") {
console.log(`${event.eventType}: ${event.path}`);
eventCount++;
// Stop after 100 events
if (eventCount >= 100) {
break; // Breaking out of the loop cancels the stream
}
}
}
console.log("Watch stopped");import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";
const stream = await sandbox.watch("/workspace/src");
let eventCount = 0;
for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
if (event.type === "event") {
console.log(`${event.eventType}: ${event.path}`);
eventCount++;
// Stop after 100 events
if (eventCount >= 100) {
break; // Breaking out of the loop cancels the stream
}
}
}
console.log("Watch stopped");Рекомендации
Используйте фильтрацию на стороне сервера
Фильтруйте с помощью include или exclude шаблоны, а не фильтровать события в JavaScript. Серверная фильтрация происходит на уровне inotify, что уменьшает количество событий, передаваемых по сети.
import { parseSSEStream } from "@cloudflare/sandbox";
// Efficient: filtering happens at the inotify level
const stream = await sandbox.watch("/workspace/src", {
include: ["*.ts"],
});
// Less efficient: all events are sent and then filtered in JavaScript
const stream2 = await sandbox.watch("/workspace/src");
for await (const event of parseSSEStream(stream2)) {
if (event.type === "event") {
if (!event.path.endsWith(".ts")) continue;
// Handle event
}
}import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";
// Efficient: filtering happens at the inotify level
const stream = await sandbox.watch("/workspace/src", {
include: ["*.ts"],
});
// Less efficient: all events are sent and then filtered in JavaScript
const stream2 = await sandbox.watch("/workspace/src");
for await (const event of parseSSEStream<FileWatchSSEEvent>(stream2)) {
if (event.type === "event") {
if (!event.path.endsWith(".ts")) continue;
// Handle event
}
}Обработка ошибок при работе с событиями
Ошибки в обработчике событий не останавливают поток наблюдения. Оберните логику обработчика в try...catch чтобы избежать необработанных исключений:
import { parseSSEStream } from "@cloudflare/sandbox";
const stream = await sandbox.watch("/workspace/src");
for await (const event of parseSSEStream(stream)) {
if (event.type === "event") {
try {
await handleFileChange(event.eventType, event.path);
} catch (error) {
console.error(
`Failed to handle ${event.eventType} for ${event.path}:`,
error,
);
// Continue processing events
}
}
if (event.type === "error") {
console.error("Watch error:", event.error);
}
}import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";
const stream = await sandbox.watch("/workspace/src");
for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
if (event.type === "event") {
try {
await handleFileChange(event.eventType, event.path);
} catch (error) {
console.error(
`Failed to handle ${event.eventType} for ${event.path}:`,
error,
);
// Continue processing events
}
}
if (event.type === "error") {
console.error("Watch error:", event.error);
}
}Убедитесь, что каталоги существуют, прежде чем начать отслеживание
Отслеживание несуществующего пути возвращает ошибку. Прежде чем начать отслеживание, убедитесь, что путь существует:
const watchPath = "/workspace/src";
const result = await sandbox.exists(watchPath);
if (!result.exists) {
await sandbox.mkdir(watchPath, { recursive: true });
}
const stream = await sandbox.watch(watchPath, {
include: ["*.ts"],
});const watchPath = "/workspace/src";
const result = await sandbox.exists(watchPath);
if (!result.exists) {
await sandbox.mkdir(watchPath, { recursive: true });
}
const stream = await sandbox.watch(watchPath, {
include: ["*.ts"],
});Устранение неполадок
Высокая загрузка CPU
Если отслеживание больших каталогов вызывает проблемы с производительностью:
- Используйте конкретные
includeшаблоны вместо отслеживания всего подряд - Исключайте большие каталоги, например
node_modulesиdist - Отслеживайте конкретные подкаталоги вместо всего проекта
- Используйте
recursive: falseдля базового мониторинга
Ошибки «путь не найден»
Все пути должны существовать и находиться внутри /workspace. Относительные пути разрешаются от /workspace.
Дополнительные материалы
- Справочник по File Watching API : полная документация по API и типам
- Руководство по управлению файлами : операции с файлами
- Руководство по фоновым процессам : долго работающие процессы
- Руководство по потоковому выводу : обработка вывода в реальном времени