Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@switchify/scanning",
"version": "0.2.0",
"version": "0.3.0",
"description": "Switch scanning for React and React Native, ported from Switchify PC.",
"license": "AGPL-3.0-only",
"type": "module",
Expand Down
11 changes: 9 additions & 2 deletions src/react/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,17 @@ export function ScanProvider({
return <ControllerContext.Provider value={controller}>{children}</ControllerContext.Provider>;
}

let inert: ScanController | undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Old control IDs accumulate

The module-wide fallback retains every distinct item and group ID it has registered. ScanController.sequenceOf() stores IDs in sequences, but unregistering removes only the live registrations. Because this fallback lasts for the app session, provider-less lists with changing IDs keep growing that map after their rows unmount. Rendering and cleaning up two independent roots retained 400 historical IDs with no live items, groups, or subscriptions. This is a non-blocking memory-retention concern for long-running apps; avoid storing registration history in the inert fallback.

Artifacts

Evidence from the check

  • This executed integration test changes item and group IDs across two independent roots and inspects controller maps after cleanup, proving historical IDs remain.

Evidence from the check

  • This executed runner extracts the parent React module and captures both test runs with their commands, working directory, and exit codes, making the comparison reproducible.

Evidence from the check

  • The before test executes this parent-revision module with relocated core import paths, showing providerless rendering was previously rejected.

Command output from the check

  • The runner captured revision hashes, the React diff, numbered registration and sequence code, and repository status, tying the runtime result to the requested source location.

Command output from the check

  • The parent-revision test rendered the providerless component tree and captured the expected provider-required exception, showing no fallback controller was created.

Command output from the check

  • The current-revision test captured every generation and unmount checkpoint, showing sequence retention grows from 200 to 400 IDs while live registrations return to zero.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/react/index.tsx
Line: 93

Comment:
**Old control IDs accumulate**

The module-wide fallback retains every distinct item and group ID it has registered. `ScanController.sequenceOf()` stores IDs in `sequences`, but unregistering removes only the live registrations. Because this fallback lasts for the app session, provider-less lists with changing IDs keep growing that map after their rows unmount. Rendering and cleaning up two independent roots retained 400 historical IDs with no live items, groups, or subscriptions. This is a non-blocking memory-retention concern for long-running apps; avoid storing registration history in the inert fallback.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.


/**
* Outside a ScanProvider the hooks use one controller that is never started, so a
* shared component can call them whether or not its screen is scanned.
*/
export function useScanController(): ScanController {
const controller = useContext(ControllerContext);
if (!controller) throw new Error("Scanning hooks must be used inside a ScanProvider.");
return controller;
if (controller) return controller;
inert ??= new ScanController();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Provider-less controls can start scanning

The fallback is a working, shared ScanController, not an inert one. Outside a ScanProvider, useScanner().start() or dispatch("select") starts it, and a bound key press through useKeyboardSwitches does the same. Controls in unrelated provider-less React trees then share its highlights and activation callbacks. This was reproduced with two separate roots: keyboard input highlighted and activated the second root's control without clicking it. Make the fallback unable to start scanning or activate items, and cover these entry points before merging.

Artifacts

▶ Recording of the check

  • Rendered two separate providerless React roots and pressed Space, Enter, and Space on the parent revision, showing hook errors rather than a registered scanner.

▶ Recording of the check

  • Pressed Space, Enter, and Space on the PR revision, showing scanning start, Tree B highlighting, and its handler count increase without clicking its button.

Both providerless roots report hook errors before the PR

  • Captured the parent revision after the keyboard sequence, showing that providerless hooks reject mounting instead of enabling scanning.

Unrelated Tree B is highlighted after the PR

  • Captured the PR revision after Space and Enter, showing an active scanner highlighting the separate providerless root.

Command output from the check

  • Ran all three providerless entry-point cases against the parent source and captured passing assertions that both roots throw before registration.

Command output from the check

  • Ran all three entry-point cases against the PR source and captured passing assertions for cross-root highlighting and handler invocation.

Command output from the check

  • Executed the browser harness and captured DOM observations for both revisions, confirming that the PR enables unrelated-tree activation.

Command output from the check

  • Printed revision hashes, both React implementations, related numbered source, and the production diff, locating F1 at line 102 with no production changes.

Evidence from the check

  • Contains the rendered two-root tests and observed-state logging for public start, public select, and keyboard activation, making the reproduction rerunnable.

Evidence from the check

  • Loads the exact parent React source for the before run without editing production files, keeping the two test runs comparable.

Evidence from the check

  • Runs the supplied command and saves its actual output with command, working directory, and exit code, preserving execution provenance.

Evidence from the check

  • Provides the two-root layout and scan-state styling used in both recordings, making the unrelated-tree highlight visible.

Evidence from the check

  • Mounts two separate roots with real scanning hooks and rendered callback counters, exposing cross-root activation without an item click handler.

Evidence from the check

  • Serves each source revision, performs keyboard and public-start interactions, checks rendered results, and captures videos and posters, reproducing F1 in Chromium.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/react/index.tsx
Line: 102

Comment:
**Provider-less controls can start scanning**

The fallback is a working, shared `ScanController`, not an inert one. Outside a `ScanProvider`, `useScanner().start()` or `dispatch("select")` starts it, and a bound key press through `useKeyboardSwitches` does the same. Controls in unrelated provider-less React trees then share its highlights and activation callbacks. This was reproduced with two separate roots: keyboard input highlighted and activated the second root's control without clicking it. Make the fallback unable to start scanning or activate items, and cover these entry points before merging.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

return inert;
}

export function useScanSnapshot(): ScanSnapshot {
Expand Down
6 changes: 6 additions & 0 deletions test/dom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,9 @@ test("the latest handler runs without re-registering", async () => {
act(() => scanner.dispatch("select"));
expect(screen.getByTestId("count").textContent).toBe("2");
});

test("hooks outside a provider render without scanning", async () => {
render(<Item id="alone" />);
await flush();
expect(highlighted("alone")).toBe(false);
});
Loading