-
Notifications
You must be signed in to change notification settings - Fork 438
fix(pi-fff): share finders across in-process sessions #801
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -45,6 +45,25 @@ function makePickers() { | |||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| describe("process-wide finder sharing", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| test("one factory releases each acquisition independently", async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| created.length = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const pickers = makePickers(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const [first, second] = await Promise.all([ | ||||||||||||||||||||||||||||||||||||||||||||||||
| pickers.create({ basePath: "/workspace" }), | ||||||||||||||||||||||||||||||||||||||||||||||||
| pickers.create({ basePath: "/workspace" }), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| expect(created).toHaveLength(1); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(first).toBe(second); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| pickers.release(first); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(created[0].isDestroyed).toBe(false); | ||||||||||||||||||||||||||||||||||||||||||||||||
| pickers.release(second); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(created[0].isDestroyed).toBe(true); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| describe("AuxFinderPool concurrent dedup (#746)", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| test("two concurrent acquires for same root share one finder", async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| created.length = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -58,6 +77,21 @@ describe("AuxFinderPool concurrent dedup (#746)", () => { | |||||||||||||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(created.length).toBe(1); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(a.finder).toBe(b.finder); | ||||||||||||||||||||||||||||||||||||||||||||||||
| pool.destroy(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| test("destroying a pool releases a finder whose scan is still starting", async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| created.length = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const pool = new AuxFinderPool({ | ||||||||||||||||||||||||||||||||||||||||||||||||
| enableFsRootScanning: false, | ||||||||||||||||||||||||||||||||||||||||||||||||
| pickers: makePickers(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const pending = pool.acquire("/Users/pending"); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| pool.destroy(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(pending).rejects.toThrow("destroyed during initialization"); | ||||||||||||||||||||||||||||||||||||||||||||||||
| await pending.catch(() => undefined); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(created[0].isDestroyed).toBe(true); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+83
to
+94
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🌐 Web query:
💡 Result: Yes, when using expect(...).rejects in bun:test, you must use await to correctly assert that a promise rejects [1]. While early versions of Bun had issues with this implementation [2], the current official documentation and testing patterns confirm that you should await the expectation [1]. For example, when handling asynchronous errors, the recommended pattern is: test("should handle async errors", async => { await expect(async => { await fetchUser("invalid-id"); }).rejects.toThrow("User not found"); }); In this pattern, the await ensures that the test runner waits for the asynchronous operation (the function passed to expect) to complete and reject before finalizing the assertion [1]. If you do not use await, the test may complete before the rejection occurs, potentially causing the test to pass incorrectly or fail to catch the error. Citations:
🏁 Script executed: #!/bin/bash
set -eu
target="packages/pi-fff/test/aux-dedup.test.ts"
printf '%s\n' '--- target context ---'
sed -n '1,125p' "$target"
printf '%s\n' '--- package/test configuration ---'
for f in package.json packages/pi-fff/package.json bunfig.toml; do
if [ -f "$f" ]; then
echo "### $f"
sed -n '1,180p' "$f"
fi
done
printf '%s\n' '--- nearby rejects assertion patterns ---'
rg -n -C 2 'expect\\([^)]*\\)\\.rejects|\\.catch\\(\\(\\) => undefined\\)' packages/pi-fff/testRepository: dmtrKovalenko/fff Length of output: 4770 🏁 Script executed: #!/bin/bash
set -eu
if ! command -v bun >/dev/null 2>&1; then
echo "bun is unavailable"
exit 0
fi
echo "--- bun version ---"
bun --version
echo "--- unawaited rejects assertion ---"
set +e
bun -e '
import { expect } from "bun:test";
const pending = Promise.resolve("resolved");
expect(pending).rejects.toThrow("must reject");
await new Promise((resolve) => setTimeout(resolve, 25));
console.log("completed");
' >/tmp/bun-unawaited.out 2>&1
status=$?
set -e
echo "exit=$status"
cat /tmp/bun-unawaited.out
echo "--- awaited rejects assertion ---"
set +e
bun -e '
import { expect } from "bun:test";
const pending = Promise.resolve("resolved");
await expect(pending).rejects.toThrow("must reject");
' >/tmp/bun-awaited.out 2>&1
status=$?
set -e
echo "exit=$status"
cat /tmp/bun-awaited.outRepository: dmtrKovalenko/fff Length of output: 174 Await the rejection assertion
💚 Fix- expect(pending).rejects.toThrow("destroyed during initialization");
- await pending.catch(() => undefined);
+ await expect(pending).rejects.toThrow("destroyed during initialization");📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| test("sequential acquire after in-flight one resolves still reuses", async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -72,5 +106,6 @@ describe("AuxFinderPool concurrent dedup (#746)", () => { | |||||||||||||||||||||||||||||||||||||||||||||||
| const third = await pool.acquire("/Users/x"); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(created.length).toBe(1); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(third.root).toBe("/Users/x"); | ||||||||||||||||||||||||||||||||||||||||||||||||
| pool.destroy(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
One dead path leaks a global reference.
If the shared entry for
ownership.keywas replaced (stale destroyed entry deleted inacquire, then a new finder registered under the same key),shared.finder !== finderreturns early. The old finder is never destroyed here, and no further owner can trigger it. Small window, but the finder object stays alive with native resources. Destroy the finder when the shared entry no longer points at it.🐛 Suggested guard
🤖 Prompt for AI Agents