Repository: saghul/txiki.js (2026-03-26T10:04:17Z)

Issue:
FileWatcher stop working if the FileWatcher object is collected by GC
The `FileWatcher` will stop working if the FileWatcher object is collected by GC, it should keep process running and only be cleaned up when `FW.close()` is called, just like `tjs.serve`.
```ts
// define a variable to hold a reference of FileWatcher object
const h = tjs.watch("./test.txt", () => {
	console.log("works");
})

tjs.watch("./test.txt", () => {
	console.log("disposed");
})

// Process will exit if there is only a FileWatcher is running, use a infinite promise to keep process running
await new Promise(res => {});
```



Discussion:

Patch:
diff --git a/src/mod_fswatch.c b/src/mod_fswatch.c
index 114c6d76..a409587f 100644
--- a/src/mod_fswatch.c
+++ b/src/mod_fswatch.c
@@ -30,6 +30,7 @@ typedef struct {
     JSContext *ctx;
     uv_fs_event_t handle;
     JSValue callback;
+    JSValue this_val;
     int closed;
     int finalized;
 } TJSFsWatch;
@@ -60,6 +61,7 @@ static void tjs_fswatch_finalizer(JSRuntime *rt, JSValue val) {
     TJSFsWatch *fw = tjs_fswatch_get(val);
     if (fw) {
         JS_FreeValueRT(rt, fw->callback);
+        JS_FreeValueRT(rt, fw->this_val);
         fw->finalized = 1;
         if (fw->closed) {
             tjs__free(fw);
@@ -88,6 +90,14 @@ static JSValue tjs_fswatch_close(JSContext *ctx, JSValue this_val, int argc, JSV
         return JS_EXCEPTION;
     }
     maybe_close(fw);
+
+    /* Release the GC-prevention self-reference so the object can be collected. */
+    if (!JS_IsUndefined(fw->this_val)) {
+        JSValue val = fw->this_val;
+        fw->this_val = JS_UNDEFINED;
+        JS_FreeValue(ctx, val);
+    }
+
     return JS_UNDEFINED;
 }
 
@@ -208,6 +218,10 @@ static JSValue tjs_fs_watch(JSContext *ctx, JSValue this_val, int argc, JSValue
     fw->callback = JS_DupValue(ctx, argv[1]);
 
     JS_SetOpaque(obj, fw);
+
+    /* Prevent GC while the watcher is active. */
+    fw->this_val = JS_DupValue(ctx, obj);
+
     return obj;
 }
 


Test patch:
diff --git a/tests/test-fswatch-gc.js b/tests/test-fswatch-gc.js
new file mode 100644
index 00000000..ca4f8073
--- /dev/null
+++ b/tests/test-fswatch-gc.js
@@ -0,0 +1,46 @@
+import assert from 'tjs:assert';
+import path from 'tjs:path';
+
+// Test that a FileWatcher survives GC even when no JS reference is held.
+// Regression test for https://github.com/saghul/txiki.js/issues/880
+
+const encoder = new TextEncoder();
+
+let eventCount = 0;
+
+const tmpDir = await tjs.makeTempDir('test_dirXXXXXX');
+
+// Create watcher without holding a reference.
+(function() {
+    tjs.watch(tmpDir, (filePath, event) => {
+        eventCount++;
+    });
+})();
+
+// Force GC to collect unreferenced objects.
+tjs.engine.gc.run();
+
+async function sleep(ms) {
+    return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+// Give the watcher time to settle.
+await sleep(1000);
+
+// Trigger a file change — the watcher should still be alive.
+const f = await tjs.makeTempFile(path.join(tmpDir, 'test_fileXXXXXX'));
+const p = f.path;
+await f.write(encoder.encode('hello'));
+await f.close();
+
+await sleep(1000);
+
+assert.ok(eventCount > 0, `expected events after GC, got ${eventCount}`);
+
+// Clean up: we can't call .close() on the watcher since we don't have a reference,
+// but we can clean up the temp files.
+await tjs.remove(p);
+await tjs.remove(tmpDir);
+
+tjs.exit(0);
+