Skip to content

[codex] fix mobile search on older iOS#449

Merged
tomusdrw merged 8 commits into
mainfrom
issue-446
Jun 23, 2026
Merged

[codex] fix mobile search on older iOS#449
tomusdrw merged 8 commits into
mainfrom
issue-446

Conversation

@tomusdrw

@tomusdrw tomusdrw commented Jun 22, 2026

Copy link
Copy Markdown
Member

Summary

  • Add a startup ReadableStream async-iteration polyfill for Safari versions that expose streams without .values() / [Symbol.asyncIterator].
  • Keep pdf.js search result aggregation tolerant of sparse/in-progress pageMatches arrays.
  • Remove the temporary RegExp.escape polyfill so this branch stays minimal while Safari testing confirms whether it is needed.

Why

pdf.js uses for await (... of readableStream) inside PDFPageProxy.getTextContent. On affected Safari builds, missing stream async iteration causes text extraction to fail with TypeError: undefined is not a function, so search indexing returns zero results.

Fixes #446.

Validation

  • WebKit simulation with ReadableStream.prototype.values and [Symbol.asyncIterator] deleted before app load: protocol returns 47 results in 18 sections.
  • npm run test -- --run
  • npm run qa
  • npm run build
  • pre-push npm run qa hook passed

Summary by CodeRabbit

  • New Features

    • Improved search result tracking and reporting with accurate per-page match summaries
    • Enhanced PDF text extraction and search compatibility on Safari variants
  • Tests

    • Added test coverage for search match summarization logic
    • Added test coverage for cross-browser stream compatibility

@netlify

netlify Bot commented Jun 22, 2026

Copy link
Copy Markdown

Deploy Preview for graypaper-reader ready!

Name Link
🔨 Latest commit 747e76b
🔍 Latest deploy log https://app.netlify.com/projects/graypaper-reader/deploys/6a3a4b9dc36098000810d18e
😎 Deploy Preview https://deploy-preview-449--graypaper-reader.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bab9aaec-1f1d-4c2f-bfd2-04e3e3be9321

📥 Commits

Reviewing files that changed from the base of the PR and between 537618d and 747e76b.

📒 Files selected for processing (2)
  • src/components/Search/Search.tsx
  • src/main.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/Search/Search.tsx

📝 Walkthrough

Walkthrough

Adds a ReadableStream async-iterator polyfill installed at app startup for pdf.js compatibility on certain Safari versions. Introduces a getMatchSummary helper in Search.tsx that aggregates per-page match arrays into a structured summary. Adds lastSearchQuery and hasActiveQuery refs to handle empty-query state clearing and search dispatch logic.

Changes

ReadableStream Async-Iterator Polyfill

Layer / File(s) Summary
Polyfill implementation
src/utils/readableStreamAsyncIteratorPolyfill.ts
readableStreamValues<T>() implements an AsyncIterableIterator over a stream reader with next()/return() managing lock release and optional cancellation. installReadableStreamAsyncIteratorPolyfill() conditionally defines ReadableStream.prototype.values and ReadableStream.prototype[Symbol.asyncIterator] via Object.defineProperty when absent.
Polyfill tests
src/utils/readableStreamAsyncIteratorPolyfill.spec.ts
Tests snapshot and restore prototype descriptors after each run. One test deletes native implementations, invokes the installer, and verifies stream iteration yields enqueued values. Another test verifies the polyfill does not overwrite pre-existing implementations.
Startup wiring
src/main.tsx
Imports and immediately invokes installReadableStreamAsyncIteratorPolyfill before any React rendering.

Search Match Summarization and Query State Tracking

Layer / File(s) Summary
getMatchSummary helper and tests
src/components/Search/Search.tsx, src/components/Search/Search.spec.ts
Adds exported getMatchSummary converting a sparse ArrayLike of per-page match arrays into { count, pagesAndCount }, skipping undefined/empty pages. Tests verify both continuous and sparse undefined entries.
Query state refs and effect refactoring
src/components/Search/Search.tsx
Adds lastSearchQuery and hasActiveQuery refs. The query-driven effect now explicitly handles empty queries by clearing state, calling onSearchFinished(false), and optionally dispatching a blank search. The updateMatches handler uses getMatchSummary and gates onSearchFinished on hasActiveQuery.current.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • FluffyLabs/graypaper-reader#236: Modifies Search.tsx onSearchFinished signaling and updatefindmatchescount / pageMatches processing, overlapping with this PR's empty-query handling and match summarization changes.
  • FluffyLabs/graypaper-reader#391: Updates matches.pagesAndCount and match counter logic in Search.tsx, directly overlapping with the new getMatchSummary aggregation introduced here.

Suggested reviewers

  • DrEverr
  • mateuszsikora

Poem

🐇 A polyfill hops into Safari's stream,
Async iterators now work like a dream!
The search finds its matches, page after page,
Empty queries bow out from the stage.
With refs tracking queries, we never get lost —
No broken PDF search, whatever the cost! 🍃

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[codex] fix mobile search on older iOS' is directly related to the main change—implementing a ReadableStream async-iteration polyfill to fix mobile search on older iOS versions.
Linked Issues check ✅ Passed The PR addresses issue #446 (Search does not work on mobile) by implementing a ReadableStream async-iteration polyfill for Safari compatibility and improving search result aggregation, meeting the objective to restore search functionality on older iOS.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing mobile search: the polyfill implementation, test coverage, search integration, and startup initialization align with the issue resolution scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-446

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Visual Regression Test Report ✅ Passed

Github run id: 28014861182

🔗 Artifacts: Download

tomusdrw added 2 commits June 22, 2026 17:11
# Conflicts:
#	tools/snapshot-tests/tests/split-screen.spec.ts-snapshots/split-header-options-menu-dark-mode-linux.png
#	tools/snapshot-tests/tests/split-screen.spec.ts-snapshots/split-header-options-menu-light-mode-linux.png
#	tools/snapshot-tests/tests/split-screen.spec.ts-snapshots/split-header-version-dropdown-light-mode-linux.png
#	tools/snapshot-tests/tests/split-screen.spec.ts-snapshots/split-sidebar-overlay-closed-dark-mode-linux.png
#	tools/snapshot-tests/tests/split-screen.spec.ts-snapshots/split-sidebar-overlay-closed-light-mode-linux.png
#	tools/snapshot-tests/tests/split-screen.spec.ts-snapshots/split-sidebar-overlay-dark-mode-linux.png
#	tools/snapshot-tests/tests/split-screen.spec.ts-snapshots/split-view-closed-light-mode-linux.png
#	tools/snapshot-tests/tests/split-screen.spec.ts-snapshots/split-view-opened-dark-mode-linux.png
#	tools/snapshot-tests/tests/split-screen.spec.ts-snapshots/split-view-via-url-light-mode-linux.png
@tomusdrw tomusdrw marked this pull request as ready for review June 23, 2026 07:17

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/Search/Search.tsx`:
- Around line 224-249: The issue is that when the query becomes empty, the code
calls search("") on lines 231-233, which can trigger the updateMatches callback
and cause onSearchFinished(true) to be called, overwriting the
onSearchFinished(false) that was already called on line 230 during the clear
path. To fix this, remove the search("") call block (lines 231-233) entirely, or
introduce a ref flag to track that we are in a clearing state and use it in the
updateMatches callback to prevent onSearchFinished(true) from being called when
clearing. This ensures the search state remains cleared and does not flip back
to active after the empty-query clear.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 51d9fdf8-c7e5-494f-b92b-4bdab7402244

📥 Commits

Reviewing files that changed from the base of the PR and between 805b44e and 7e5b111.

📒 Files selected for processing (2)
  • src/components/Search/Search.spec.ts
  • src/components/Search/Search.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/components/Search/Search.spec.ts

Comment thread src/components/Search/Search.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/utils/readableStreamAsyncIteratorPolyfill.ts (1)

18-31: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Release the reader lock when read() rejects.

If reader.read() rejects (errored stream), isDone stays false and the lock is never released, leaking the reader and leaving the stream locked. The native async iterator releases the lock and marks itself done on error.

♻️ Proposed fix
     async next() {
       if (isDone) {
         return { done: true, value: undefined as T };
       }

-      const result = await reader.read();
+      let result: ReadableStreamReadResult<T>;
+      try {
+        result = await reader.read();
+      } catch (error) {
+        isDone = true;
+        reader.releaseLock();
+        throw error;
+      }
       if (result.done) {
         isDone = true;
         reader.releaseLock();
         return { done: true, value: undefined as T };
       }

       return { done: false, value: result.value };
     },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/readableStreamAsyncIteratorPolyfill.ts` around lines 18 - 31, The
next() method does not handle errors when reader.read() rejects. When an error
occurs during read(), isDone remains false and the reader lock is never
released, causing a resource leak. Wrap the reader.read() call in a try-catch
block that catches any errors, sets isDone to true, calls reader.releaseLock(),
and then re-throws the error to propagate it to the caller, ensuring the reader
lock is always released whether the read succeeds or fails.
src/utils/readableStreamAsyncIteratorPolyfill.spec.ts (1)

34-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider covering the early-termination / cancel path.

The return() branch (lock release + reader.cancel, and the preventCancel option) is the most intricate logic in the polyfill but is currently untested. A test that breaks out of the for await loop and asserts the lock is released / cancel honored would close the gap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/readableStreamAsyncIteratorPolyfill.spec.ts` around lines 34 - 53,
Add a new test case that covers the early-termination path of the async iterator
polyfill. Create a test that instantiates a ReadableStream and uses a for await
loop to iterate over its values, but breaks out of the loop before all values
are consumed (simulating early termination). Verify that the lock is properly
released after the break by checking that subsequent operations on the reader
work correctly, and assert that the reader.cancel() method is called/honored
when the iteration is terminated early via the return() method in the polyfill.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/utils/readableStreamAsyncIteratorPolyfill.spec.ts`:
- Around line 34-53: Add a new test case that covers the early-termination path
of the async iterator polyfill. Create a test that instantiates a ReadableStream
and uses a for await loop to iterate over its values, but breaks out of the loop
before all values are consumed (simulating early termination). Verify that the
lock is properly released after the break by checking that subsequent operations
on the reader work correctly, and assert that the reader.cancel() method is
called/honored when the iteration is terminated early via the return() method in
the polyfill.

In `@src/utils/readableStreamAsyncIteratorPolyfill.ts`:
- Around line 18-31: The next() method does not handle errors when reader.read()
rejects. When an error occurs during read(), isDone remains false and the reader
lock is never released, causing a resource leak. Wrap the reader.read() call in
a try-catch block that catches any errors, sets isDone to true, calls
reader.releaseLock(), and then re-throws the error to propagate it to the
caller, ensuring the reader lock is always released whether the read succeeds or
fails.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 28e54ac3-cfad-43eb-a473-078dfa2db67e

📥 Commits

Reviewing files that changed from the base of the PR and between 7e5b111 and 537618d.

📒 Files selected for processing (3)
  • src/main.tsx
  • src/utils/readableStreamAsyncIteratorPolyfill.spec.ts
  • src/utils/readableStreamAsyncIteratorPolyfill.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main.tsx

@tomusdrw tomusdrw merged commit 2cbbca4 into main Jun 23, 2026
9 checks passed
@tomusdrw tomusdrw deleted the issue-446 branch June 23, 2026 09:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Search does not work on mobile

1 participant