or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

build.mdcli.mdfiltering.mdindex.mdmiddleware.mdserver.mdutils.md

server.mddocs/

0

# File Watching & Server

1

2

The server component provides file watching capabilities and WebSocket-based hot reloading functionality. It monitors file system changes and communicates with browser clients to trigger automatic page reloads.

3

4

## Capabilities

5

6

### RebuildServer Class

7

8

The main server class that handles file watching and WebSocket connections for hot reloading.

9

10

```python { .api }

11

class RebuildServer:

12

def __init__(

13

self,

14

paths: list[os.PathLike[str]],

15

ignore_filter: IgnoreFilter,

16

change_callback: Callable[[Sequence[Path]], None]

17

) -> None:

18

"""

19

Initialize file watching server.

20

21

Parameters:

22

- paths: list[os.PathLike[str]] - Directories to watch for changes

23

- ignore_filter: IgnoreFilter - Filter for ignoring specific files/patterns

24

- change_callback: Callable[[Sequence[Path]], None] - Function called when changes detected

25

"""

26

27

@asynccontextmanager

28

async def lifespan(self, _app) -> AbstractAsyncContextManager[None]:

29

"""

30

ASGI lifespan context manager for server startup/shutdown.

31

32

Manages the file watching task lifecycle:

33

- Starts file watching on application startup

34

- Stops file watching on application shutdown

35

36

Parameters:

37

- _app: ASGI application instance (unused)

38

39

Yields:

40

- None - Context for application lifespan

41

"""

42

43

async def main(self) -> None:

44

"""

45

Main server event loop.

46

47

Coordinates file watching and shutdown handling:

48

- Creates file watching task

49

- Creates shutdown waiting task

50

- Cancels pending tasks on completion

51

52

Returns:

53

- None

54

"""

55

56

async def watch(self) -> None:

57

"""

58

File watching loop using watchfiles.

59

60

Monitors configured directories for changes:

61

- Filters changes through ignore_filter

62

- Calls change_callback with changed paths

63

- Runs callback in ProcessPoolExecutor for isolation

64

- Sets internal flag to notify WebSocket clients

65

66

Returns:

67

- None

68

"""

69

70

async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:

71

"""

72

ASGI application callable for WebSocket connections.

73

74

Handles WebSocket connections from browser clients:

75

- Accepts WebSocket connections

76

- Manages client connection lifecycle

77

- Coordinates reload notifications and disconnect handling

78

79

Parameters:

80

- scope: ASGI scope dict with connection info

81

- receive: ASGI receive callable for messages

82

- send: ASGI send callable for responses

83

84

Returns:

85

- None

86

"""

87

88

async def watch_reloads(self, ws: WebSocket) -> None:

89

"""

90

Send reload notifications to WebSocket client.

91

92

Waits for file change events and sends reload messages:

93

- Blocks until file changes detected

94

- Sends "refresh" message to browser client

95

- Resets change flag for next iteration

96

97

Parameters:

98

- ws: WebSocket - Connected client WebSocket

99

100

Returns:

101

- None

102

"""

103

104

@staticmethod

105

async def wait_client_disconnect(ws: WebSocket) -> None:

106

"""

107

Wait for WebSocket client to disconnect.

108

109

Monitors WebSocket for client disconnection:

110

- Iterates over incoming messages until connection closes

111

- Provides graceful handling of client disconnects

112

113

Parameters:

114

- ws: WebSocket - Connected client WebSocket

115

116

Returns:

117

- None

118

"""

119

```

120

121

## Usage Examples

122

123

### Basic File Watching Setup

124

125

```python

126

import asyncio

127

from pathlib import Path

128

from sphinx_autobuild.server import RebuildServer

129

from sphinx_autobuild.filter import IgnoreFilter

130

131

def build_callback(changed_paths):

132

"""Called when files change."""

133

print(f"Files changed: {[str(p) for p in changed_paths]}")

134

# Trigger rebuild logic here

135

136

# Setup file watching

137

watch_dirs = [Path('docs'), Path('source')]

138

ignore_filter = IgnoreFilter(['.git', '__pycache__'], [r'\.tmp$'])

139

server = RebuildServer(watch_dirs, ignore_filter, build_callback)

140

141

# Run the file watcher

142

async def run_watcher():

143

await server.main()

144

145

# asyncio.run(run_watcher())

146

```

147

148

### ASGI Application Integration

149

150

```python

151

from starlette.applications import Starlette

152

from starlette.middleware import Middleware

153

from starlette.routing import Mount, WebSocketRoute

154

from starlette.staticfiles import StaticFiles

155

from sphinx_autobuild.server import RebuildServer

156

from sphinx_autobuild.middleware import JavascriptInjectorMiddleware

157

158

# Create server instance

159

server = RebuildServer(watch_dirs, ignore_filter, build_callback)

160

161

# Create ASGI application

162

app = Starlette(

163

routes=[

164

WebSocketRoute("/websocket-reload", server, name="reload"),

165

Mount("/", app=StaticFiles(directory="_build/html", html=True), name="static"),

166

],

167

middleware=[Middleware(JavascriptInjectorMiddleware, ws_url="127.0.0.1:8000")],

168

lifespan=server.lifespan,

169

)

170

```

171

172

### Custom Change Handling

173

174

```python

175

from pathlib import Path

176

from sphinx_autobuild.server import RebuildServer

177

from sphinx_autobuild.filter import IgnoreFilter

178

179

class CustomChangeHandler:

180

def __init__(self):

181

self.change_count = 0

182

183

def __call__(self, changed_paths):

184

"""Custom change callback with logging and filtering."""

185

self.change_count += 1

186

187

# Filter to only .rst and .md files

188

docs_changes = [

189

p for p in changed_paths

190

if p.suffix in ['.rst', '.md', '.py']

191

]

192

193

if docs_changes:

194

print(f"Build #{self.change_count}: {len(docs_changes)} documentation files changed")

195

# Trigger actual build

196

self.rebuild_docs(docs_changes)

197

else:

198

print(f"Change #{self.change_count}: Non-documentation files changed, skipping build")

199

200

def rebuild_docs(self, paths):

201

# Custom build logic

202

pass

203

204

# Use custom handler

205

handler = CustomChangeHandler()

206

server = RebuildServer([Path('docs')], IgnoreFilter([], []), handler)

207

```

208

209

## WebSocket Protocol

210

211

### Client-Server Communication

212

213

The WebSocket protocol used for browser hot reloading is simple:

214

215

**Connection:**

216

- Browser connects to `/websocket-reload` endpoint

217

- Server accepts connection and starts monitoring for changes

218

219

**Messages:**

220

- **Server → Client**: `"refresh"` - Triggers browser reload

221

- **Client → Server**: Any message keeps connection alive

222

223

**Connection Lifecycle:**

224

1. Browser JavaScript establishes WebSocket connection

225

2. Server waits for file changes or client disconnect

226

3. On file change: server sends `"refresh"` message

227

4. Browser receives message and reloads page

228

5. New page establishes fresh WebSocket connection

229

230

### JavaScript Client Code

231

232

The browser client code (injected by middleware):

233

234

```javascript

235

const ws = new WebSocket("ws://127.0.0.1:8000/websocket-reload");

236

ws.onmessage = () => window.location.reload();

237

```

238

239

## File Watching Details

240

241

### Watchfiles Integration

242

243

Uses the `watchfiles` library for efficient file system monitoring:

244

245

- **Cross-platform**: Works on Windows, macOS, Linux

246

- **Efficient**: Uses native OS file watching APIs

247

- **Recursive**: Automatically watches subdirectories

248

- **Filter Integration**: Applies IgnoreFilter to all detected changes

249

250

### Change Processing

251

252

When files change:

253

254

1. **Detection**: watchfiles detects filesystem events

255

2. **Filtering**: IgnoreFilter determines if changes should be processed

256

3. **Callback Execution**: change_callback runs in ProcessPoolExecutor for isolation

257

4. **Notification**: WebSocket clients receive reload message

258

259

### Performance Characteristics

260

261

- **Asynchronous**: Non-blocking file watching and WebSocket handling

262

- **Process Isolation**: Build callbacks run in separate process to prevent blocking

263

- **Concurrent Clients**: Supports multiple browser connections simultaneously

264

- **Efficient Filtering**: File filtering happens before expensive operations

265

266

## Error Handling

267

268

### File Watching Errors

269

270

- **Permission Denied**: Graceful handling of inaccessible directories

271

- **Directory Not Found**: Validates watch paths during initialization

272

- **Filesystem Errors**: Continues watching other directories on isolated errors

273

274

### WebSocket Errors

275

276

- **Connection Failures**: Individual client disconnects don't affect server

277

- **Message Errors**: Malformed messages are ignored

278

- **Protocol Errors**: WebSocket protocol violations handled gracefully

279

280

### Callback Errors

281

282

- **Exception Isolation**: Errors in change_callback don't crash server

283

- **Process Boundaries**: ProcessPoolExecutor contains callback failures

284

- **Continued Operation**: Server continues watching after callback errors

285

286

## Integration Points

287

288

### With Build System

289

290

```python

291

from sphinx_autobuild.build import Builder

292

293

# Builder as change callback

294

builder = Builder(sphinx_args, url_host='127.0.0.1:8000',

295

pre_build_commands=[], post_build_commands=[])

296

server = RebuildServer([Path('docs')], ignore_filter, builder)

297

```

298

299

### With ASGI Framework

300

301

```python

302

from starlette.applications import Starlette

303

304

app = Starlette(

305

routes=[WebSocketRoute("/websocket-reload", server)],

306

lifespan=server.lifespan # Manages file watching lifecycle

307

)

308

```