From 93abfab6d98e20439fb66530428a64e09019c5da Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Thu, 16 Jul 2026 14:13:33 +0100 Subject: [PATCH] Shut down dynamic store watching after headless commands The chokidar watchers and syncer poll timer kept the node event loop alive forever after commands like --render and --build when a dynamic store was configured. Add a startup module to the filesystem plugin that runs after the commander completes: if no server was started (detected via th-server-command-post-start), it closes the watchers, stops advertising getUpdatedTiddlers so the syncer schedules no more polls, and drains any pending sync tasks so saves still flush before the process exits naturally. Co-Authored-By: Claude Fable 5 --- plugins/tiddlywiki/filesystem/teardown.js | 48 +++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 plugins/tiddlywiki/filesystem/teardown.js diff --git a/plugins/tiddlywiki/filesystem/teardown.js b/plugins/tiddlywiki/filesystem/teardown.js new file mode 100644 index 000000000..1476e91b5 --- /dev/null +++ b/plugins/tiddlywiki/filesystem/teardown.js @@ -0,0 +1,48 @@ +/*\ +title: $:/plugins/tiddlywiki/filesystem/teardown.js +type: application/javascript +module-type: startup + +Shut down dynamic store watching and syncer polling once all CLI commands +have completed, unless a server was started. Otherwise the chokidar watchers +and the syncer poll timer keep the node event loop alive forever after +headless commands like --render and --build. + +\*/ + +"use strict"; + +exports.name = "filesystem-teardown"; +exports.platforms = ["node"]; +exports.after = ["commands"]; +exports.synchronous = true; + +var serverStarted = false; + +if($tw.node) { + $tw.hooks.addHook("th-server-command-post-start",function() { + serverStarted = true; + }); +} + +exports.startup = function() { + var adaptor = $tw.syncadaptor; + if(serverStarted || !adaptor || adaptor.name !== "filesystem") { + return; + } + // Stop advertising poll support so the syncer doesn't reschedule + adaptor.getUpdatedTiddlers = undefined; + if(typeof adaptor.close === "function") { + adaptor.close(); + } + // Kick the syncer: a pending poll timer may be up to pollTimerInterval + // away. Clearing it and processing the queue directly drains any pending + // save tasks; with polling disabled, nothing is rescheduled afterwards. + if($tw.syncer) { + if($tw.syncer.taskTimerId) { + clearTimeout($tw.syncer.taskTimerId); + $tw.syncer.taskTimerId = null; + } + $tw.syncer.processTaskQueue(); + } +};