1
- import { type FSWatcher , type WatchListener , existsSync , watch } from "node:fs" ;
1
+ import { createHash } from "node:crypto" ;
2
+ import { type FSWatcher , existsSync , watch } from "node:fs" ;
2
3
import { access , constants , readFile , stat } from "node:fs/promises" ;
3
4
import { type IncomingMessage , type RequestListener , createServer } from "node:http" ;
4
5
import { basename , dirname , extname , join , normalize } from "node:path" ;
@@ -115,17 +116,26 @@ class Server {
115
116
// Anything else should 404; static files should be matched above.
116
117
try {
117
118
const pages = await readPages ( this . root ) ; // TODO cache? watcher?
118
- res . end (
119
- (
120
- await renderPreview ( await readFile ( path + ".md" , "utf-8" ) , {
121
- root : this . root ,
122
- path : pathname ,
123
- pages,
124
- title : ( await readConfig ( this . root ) ) ?. title ,
125
- resolver : this . _resolver !
126
- } )
127
- ) . html
128
- ) ;
119
+ const { html} = await renderPreview ( await readFile ( path + ".md" , "utf-8" ) , {
120
+ root : this . root ,
121
+ path : pathname ,
122
+ pages,
123
+ title : ( await readConfig ( this . root ) ) ?. title ,
124
+ resolver : this . _resolver !
125
+ } ) ;
126
+ const etag = `"${ createHash ( "sha256" ) . update ( html ) . digest ( "base64" ) } "` ;
127
+ res . setHeader ( "Content-Type" , "text/html; charset=utf-8" ) ;
128
+ res . setHeader ( "Date" , new Date ( ) . toUTCString ( ) ) ;
129
+ res . setHeader ( "Last-Modified" , new Date ( ) . toUTCString ( ) ) ;
130
+ res . setHeader ( "ETag" , etag ) ;
131
+ if ( req . headers [ "if-none-match" ] === etag ) {
132
+ res . statusCode = 304 ;
133
+ res . end ( ) ;
134
+ } else if ( req . method === "HEAD" ) {
135
+ res . end ( ) ;
136
+ } else {
137
+ res . end ( html ) ;
138
+ }
129
139
} catch ( error ) {
130
140
if ( ! isNodeError ( error ) || error . code !== "ENOENT" ) throw error ; // internal error
131
141
throw new HttpError ( "Not found" , 404 ) ;
@@ -214,10 +224,15 @@ function handleWatch(socket: WebSocket, options: {root: string; resolver: CellRe
214
224
} ) ;
215
225
}
216
226
217
- async function refreshMarkdown ( path : string ) : Promise < WatchListener < string > > {
227
+ async function hello ( { path, hash : initialHash } : { path : string ; hash : string } ) : Promise < void > {
228
+ if ( markdownWatcher || attachmentWatcher ) throw new Error ( "already watching" ) ;
229
+ if ( ! ( path = normalize ( path ) ) . startsWith ( "/" ) ) throw new Error ( "Invalid path: " + path ) ;
230
+ if ( path . endsWith ( "/" ) ) path += "index" ;
231
+ path += ".md" ;
218
232
let current = await readMarkdown ( path , root ) ;
233
+ if ( current . hash !== initialHash ) return void send ( { type : "reload" } ) ;
219
234
attachmentWatcher = await FileWatchers . watchAll ( path , root , current . parse , refreshAttachment ) ;
220
- return async function watcher ( event ) {
235
+ markdownWatcher = watch ( join ( root , path ) , async function watcher ( event ) {
221
236
switch ( event ) {
222
237
case "rename" : {
223
238
markdownWatcher ?. close ( ) ;
@@ -247,7 +262,7 @@ function handleWatch(socket: WebSocket, options: {root: string; resolver: CellRe
247
262
default :
248
263
throw new Error ( "Unrecognized event: " + event ) ;
249
264
}
250
- } ;
265
+ } ) ;
251
266
}
252
267
253
268
socket . on ( "message" , async ( data ) => {
@@ -256,12 +271,7 @@ function handleWatch(socket: WebSocket, options: {root: string; resolver: CellRe
256
271
console . log ( "↑" , message ) ;
257
272
switch ( message . type ) {
258
273
case "hello" : {
259
- if ( markdownWatcher || attachmentWatcher ) throw new Error ( "already watching" ) ;
260
- let { path} = message ;
261
- if ( ! ( path = normalize ( path ) ) . startsWith ( "/" ) ) throw new Error ( "Invalid path: " + path ) ;
262
- if ( path . endsWith ( "/" ) ) path += "index" ;
263
- path += ".md" ;
264
- markdownWatcher = watch ( join ( root , path ) , await refreshMarkdown ( path ) ) ;
274
+ await hello ( message ) ;
265
275
break ;
266
276
}
267
277
}
0 commit comments