vibeaudio 0.8.1 → 0.8.2

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/player.js +33 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeaudio",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "Procedural focus music while your AI coding tools (Claude Code, Codex, Cursor, Grok, Gemini, Copilot) think — a different arrangement per project.",
5
5
  "bin": {
6
6
  "vibeaudio": "bin/vibeaudio.js",
package/src/player.js CHANGED
@@ -294,9 +294,21 @@ function seedDir(seed) {
294
294
  return path.join(CACHE_DIR, `s${seed >>> 0}`);
295
295
  }
296
296
 
297
- /** Keep a small working set of projects cached; regenerating costs ~100ms. */
297
+ /**
298
+ * Keep a small working set of projects cached; regenerating costs ~100ms.
299
+ *
300
+ * A directory touched within PRUNE_MIN_AGE_MS is left alone whoever owns it.
301
+ * Another process may be part-way through a render there - it creates the
302
+ * directory, then writes its .tmp into it - and deleting it underneath that
303
+ * write throws ENOENT on the playback path, which is the one thing audio is
304
+ * never allowed to do. Time is the only signal we have across processes;
305
+ * skipping only our own directory would not have helped the other one.
306
+ */
307
+ const PRUNE_MIN_AGE_MS = 10_000;
308
+
298
309
  function pruneSeedDirs(keep = 3) {
299
310
  try {
311
+ const now = Date.now();
300
312
  const dirs = fs
301
313
  .readdirSync(CACHE_DIR, { withFileTypes: true })
302
314
  .filter((e) => e.isDirectory() && /^s\d+$/.test(e.name))
@@ -307,6 +319,7 @@ function pruneSeedDirs(keep = 3) {
307
319
  .sort((a, b) => b.mtime - a.mtime);
308
320
 
309
321
  for (const stale of dirs.slice(keep)) {
322
+ if (now - stale.mtime < PRUNE_MIN_AGE_MS) continue;
310
323
  fs.rmSync(stale.full, { recursive: true, force: true });
311
324
  }
312
325
  } catch (e) {
@@ -549,6 +562,23 @@ function writeCacheFileAtomic(filePath, buffer) {
549
562
  } catch (e) {
550
563
  fs.rmSync(tmp, { force: true });
551
564
 
565
+ // The seed directory can be pruned by another process between our mkdir
566
+ // and this write. The mtime floor in pruneSeedDirs makes that rare rather
567
+ // than impossible, so recreate the directory and try once more - a second
568
+ // ENOENT would mean a prune landed inside a window of microseconds.
569
+ if (e.code === "ENOENT") {
570
+ try {
571
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
572
+ fs.writeFileSync(tmp, buffer);
573
+ fs.renameSync(tmp, filePath);
574
+ return;
575
+ } catch (retryErr) {
576
+ fs.rmSync(tmp, { force: true });
577
+ if (fs.existsSync(filePath)) return;
578
+ throw retryErr;
579
+ }
580
+ }
581
+
552
582
  // POSIX rename replaces the destination silently. Windows does not: it
553
583
  // fails with EPERM/EACCES when another process holds the destination
554
584
  // open - which is precisely the concurrent case this function exists to
@@ -789,6 +819,8 @@ class AudioPlayer {
789
819
  module.exports = {
790
820
  AudioPlayer,
791
821
  getAudioPath,
822
+ pruneSeedDirs,
823
+ writeCacheFileAtomic,
792
824
  getChimePath,
793
825
  clearCache,
794
826
  detectPlayer,