Skip to content

Commit 72991fd

Browse files
committed
Improve server mode: fix live-reload race and unify file cleanup
- Embed last_change_time into the live-reload script at render time so the browser's initial timestamp matches the page content. This fixes a race where a change between page generation and the first poll would be silently skipped. - Call clear_file_contributions for removed files (not just changed files) and remove classes/modules from the store when no files contribute to them anymore. This correctly handles reopened classes across multiple files and improves file deletion behavior.
1 parent d11c91c commit 72991fd

1 file changed

Lines changed: 45 additions & 20 deletions

File tree

lib/rdoc/server.rb

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,27 @@
1717

1818
class RDoc::Server
1919

20-
LIVE_RELOAD_SCRIPT = <<~JS
21-
<script>
22-
(function() {
23-
var lastChange = 0;
24-
setInterval(function() {
25-
fetch('/__status').then(function(r) { return r.json(); }).then(function(data) {
26-
if (lastChange && data.last_change > lastChange) location.reload();
27-
lastChange = data.last_change;
28-
}).catch(function() {});
29-
}, 1000);
30-
})();
31-
</script>
32-
JS
20+
##
21+
# Returns a live-reload polling script with the given +last_change_time+
22+
# embedded so the browser knows the exact timestamp of the content it
23+
# received. This avoids a race where a change that occurs between page
24+
# generation and the first poll would be silently skipped.
25+
26+
def self.live_reload_script(last_change_time)
27+
<<~JS
28+
<script>
29+
(function() {
30+
var lastChange = #{last_change_time};
31+
setInterval(function() {
32+
fetch('/__status').then(function(r) { return r.json(); }).then(function(data) {
33+
if (data.last_change > lastChange) location.reload();
34+
lastChange = data.last_change;
35+
}).catch(function() {});
36+
}, 1000);
37+
})();
38+
</script>
39+
JS
40+
end
3341

3442
CONTENT_TYPES = {
3543
'.html' => 'text/html',
@@ -216,7 +224,8 @@ def serve_page(path)
216224
not_found = @generator.generate_servlet_not_found(
217225
"The page <kbd>#{ERB::Util.html_escape path}</kbd> was not found"
218226
)
219-
return [404, 'text/html', inject_live_reload(not_found || '')]
227+
t = @mutex.synchronize { @last_change_time }
228+
return [404, 'text/html', inject_live_reload(not_found || '', t)]
220229
end
221230

222231
ext = File.extname(name)
@@ -234,7 +243,7 @@ def render_page(name)
234243
result = generate_page(name)
235244
return nil unless result
236245

237-
result = inject_live_reload(result) if name.end_with?('.html')
246+
result = inject_live_reload(result, @last_change_time) if name.end_with?('.html')
238247
@page_cache[name] = result
239248
end
240249
end
@@ -273,8 +282,8 @@ def build_search_index
273282
##
274283
# Injects the live-reload polling script before +</body>+.
275284

276-
def inject_live_reload(html)
277-
html.sub('</body>', "#{LIVE_RELOAD_SCRIPT}</body>")
285+
def inject_live_reload(html, last_change_time)
286+
html.sub('</body>', "#{self.class.live_reload_script(last_change_time)}</body>")
278287
end
279288

280289
##
@@ -374,6 +383,7 @@ def reparse_and_refresh(changed_files, removed_files)
374383
removed_files.each do |f|
375384
@file_mtimes.delete(f)
376385
relative = relative_path_for(f)
386+
clear_file_contributions(relative)
377387
@store.remove_file(relative)
378388
end
379389
end
@@ -402,9 +412,10 @@ def reparse_and_refresh(changed_files, removed_files)
402412

403413
##
404414
# Removes a file's contributions (methods, constants, comments, etc.)
405-
# from its classes and modules without removing the classes themselves
406-
# from the store. This prevents duplication when the file is re-parsed
407-
# while preserving shared namespaces like +RDoc+ that span many files.
415+
# from its classes and modules. If no other files contribute to a
416+
# class or module, it is removed from the store entirely. This
417+
# prevents duplication when the file is re-parsed while preserving
418+
# shared namespaces like +RDoc+ that span many files.
408419

409420
def clear_file_contributions(relative_name)
410421
top_level = @store.files_hash[relative_name]
@@ -442,6 +453,20 @@ def clear_file_contributions(relative_name)
442453

443454
# Remove this file from the class/module's file list
444455
cm.in_files.delete(top_level)
456+
457+
# If no files contribute to this class/module anymore, remove it
458+
# from the store entirely. This handles file deletion correctly
459+
# for classes that are only defined in the deleted file, while
460+
# preserving classes that span multiple files.
461+
if cm.in_files.empty?
462+
if cm.is_a?(RDoc::NormalModule)
463+
@store.modules_hash.delete(cm.full_name)
464+
else
465+
@store.classes_hash.delete(cm.full_name)
466+
end
467+
cm.parent&.classes_hash&.delete(cm.name)
468+
cm.parent&.modules_hash&.delete(cm.name)
469+
end
445470
end
446471

447472
# Clear the TopLevel's class/module list to prevent duplicates

0 commit comments

Comments
 (0)