Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

ファイルシステムの変更を監視する

最終更新 Markdown で表示Agent セットアップ

このガイドでは、Sandbox SDK のファイル監視 API を使い、ファイルシステムの変更をリアルタイムで監視します。ファイル監視は、開発ツール、自動化ワークフロー、ファイル変更にすぐ反応するアプリケーションの構築に役立ちます。

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}`);
	}
}

ストリームは 4 種類のライフサイクルイベントを出します。

  • watching — 監視が確立されました。watchId を含みます
  • event — ファイルシステムの変更が起きました
  • 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}`);
	}
}

よく使う include パターン:

  • *.ts — TypeScript ファイル
  • *.js — JavaScript ファイル
  • *.json — JSON 設定ファイル
  • *.md — Markdown ドキュメント
  • package*.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}`);
	},
);

デバウンスしたファイル操作

変更をまとめてから処理し、過剰な操作を避けます。

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");
	}
}

監視を止める

コンテナがスリープまたはシャットダウンすると、ストリームは自然に終わります。途中で止める方法は 2 つあります。

AbortController を使う

parseSSEStreamAbortSignal を渡します。シグナルを abort するとストリームリーダーがキャンセルされ、サーバー側のクリーンアップに伝わります。消費ループの外から監視を止めたいときは、この方法を推奨します。

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");

推奨事項

サーバー側で絞り込む

JavaScript 側でイベントを絞り込むより、include または exclude パターンで絞り込みます。サーバー側の絞り込みは 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 使用率が高い

大きなディレクトリの監視で性能が落ちる場合は、次を試します。

  1. すべてを監視せず、具体的な include パターンを使います
  2. node_modulesdist のような大きなディレクトリを除外します
  3. プロジェクト全体ではなく、特定のサブディレクトリを監視します
  4. 浅い監視には recursive: false を使います

パスが見つからないエラー

パスは存在し、/workspace 内に解決される必要があります。相対パスは /workspace から解決されます。

関連リソース

役に立ちましたか?