@cueloop/daemon 0.1.0-alpha.106 → 0.1.0-alpha.107

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cueloop/daemon",
3
- "version": "0.1.0-alpha.106",
3
+ "version": "0.1.0-alpha.107",
4
4
  "description": "The cueloop session daemon and its client: socket protocol, atomic store, resumable waits",
5
5
  "homepage": "https://github.com/mmurakaru/cueloop#readme",
6
6
  "bugs": {
@@ -44,8 +44,8 @@
44
44
  "access": "public"
45
45
  },
46
46
  "dependencies": {
47
- "@cueloop/extension-api": "0.1.0-alpha.106",
48
- "@cueloop/schema": "0.1.0-alpha.106",
47
+ "@cueloop/extension-api": "0.1.0-alpha.107",
48
+ "@cueloop/schema": "0.1.0-alpha.107",
49
49
  "@pierre/diffs": "^1.5.2",
50
50
  "effect": "^4.0.4",
51
51
  "smol-toml": "^1.9.1",
@@ -117,3 +117,49 @@ test("replacing a watch during reconciliation stops the old directory traversal"
117
117
  fs.rmSync(repo, { recursive: true, force: true });
118
118
  }
119
119
  });
120
+
121
+ /** A watch listener as the runtime calls it: Bun on Linux can pass an undefined filename. */
122
+ type RuntimeWatchListener = (event: fs.WatchEventType, filename: string | null | undefined) => void;
123
+
124
+ test("a change without a filename still refreshes and watches a new directory", async () => {
125
+ const repo = fs.mkdtempSync(join(tmpdir(), "cueloop-watch-unnamed-"));
126
+ const watch = fs.watch;
127
+ const listeners = new Map<string, RuntimeWatchListener>();
128
+ let refreshes = 0;
129
+ const startup = Promise.withResolvers<void>();
130
+ const change = Promise.withResolvers<void>();
131
+ const watcher = new DiffWatcher(() => {
132
+ refreshes++;
133
+
134
+ if (refreshes === 1) startup.resolve();
135
+ else change.resolve();
136
+ });
137
+ // SAFETY: DiffWatcher always calls the string-filename, three-argument watch overload; the
138
+ // listener is typed as the runtime calls it, which may pass an undefined filename.
139
+ const spy = spyOn(fs, "watch").mockImplementation(((
140
+ path: fs.PathLike,
141
+ options: fs.WatchOptions,
142
+ listener: RuntimeWatchListener,
143
+ ) => {
144
+ listeners.set(String(path), listener);
145
+
146
+ return watch(path, options, () => {});
147
+ }) as typeof fs.watch);
148
+
149
+ try {
150
+ watcher.trackDiffRepo(repo, "session");
151
+ await startup.promise;
152
+ const onRootChange = listeners.get(repo);
153
+
154
+ expect(onRootChange).toBeDefined();
155
+ fs.mkdirSync(join(repo, "fresh"));
156
+ expect(() => onRootChange?.("rename", undefined)).not.toThrow();
157
+ await change.promise;
158
+ expect(refreshes).toBe(2);
159
+ expect(listeners.has(join(repo, "fresh"))).toBe(true);
160
+ } finally {
161
+ watcher.close();
162
+ spy.mockRestore();
163
+ fs.rmSync(repo, { recursive: true, force: true });
164
+ }
165
+ });
@@ -65,7 +65,7 @@ function spawnGit(args: string[], cwd: string): { exitCode: number; stdout: Buff
65
65
  }
66
66
 
67
67
  /** The git-metadata filenames whose change means `git diff HEAD` moved: a checkout, a commit, or a reset. */
68
- function isRefChange(filename: string): boolean {
68
+ function isRefChange(filename: string | null): boolean {
69
69
  return filename === "HEAD" || filename === "packed-refs";
70
70
  }
71
71
 
@@ -198,9 +198,12 @@ export class DiffWatcher {
198
198
  if (ALWAYS_IGNORED_DIRS.has(basename(dir)) || repoWatch.ignored.has(dir)) return true;
199
199
 
200
200
  const opened = this.watchInto(repoWatch.handles, dir, { recursive: false }, (filename) => {
201
- this.scheduleRepoRefresh(repoRoot);
202
201
  // a newly created subdirectory needs its own watch (a recursive watch got this for free)
203
- this.extendWatch(repoRoot, join(dir, filename));
202
+ if (filename !== null) this.extendWatch(repoRoot, join(dir, filename));
203
+ // no name for the change: rescan the tree on the next refresh so a new directory is not missed
204
+ else repoWatch.reconcilePending = true;
205
+
206
+ this.scheduleRepoRefresh(repoRoot);
204
207
  });
205
208
 
206
209
  if (!opened) return false;
@@ -278,17 +281,18 @@ export class DiffWatcher {
278
281
  * Open one watcher and add it to `handles`; returns whether it opened. A
279
282
  * watcher error (the path is deleted mid-review) is swallowed - hot-reload is
280
283
  * best-effort and must never take down the daemon. The inherited EventEmitter
281
- * `on` is absent from Bun's FSWatcher type.
284
+ * `on` is absent from Bun's FSWatcher type. The platform may omit the changed
285
+ * name (Bun on Linux passes undefined, Node passes null); `onFile` then gets null.
282
286
  */
283
287
  private watchInto(
284
288
  handles: FSWatcher[],
285
289
  path: string,
286
290
  options: { recursive: boolean },
287
- onFile: (filename: string) => void,
291
+ onFile: (filename: string | null) => void,
288
292
  ): boolean {
289
293
  try {
290
294
  const handle = watch(path, options, (_event, filename) => {
291
- if (filename !== null) onFile(filename.toString());
295
+ onFile(filename == null ? null : filename.toString());
292
296
  });
293
297
 
294
298
  handle.on("error", () => {});