Skip to content

Sync Reverb and Scout updates - #640

Merged
binaryfire merged 13 commits into
0.4from
upstream-sync-framework-14
Oct 2, 2026
Merged

binaryfire merged 13 commits into
0.4from
upstream-sync-framework-14

Conversation

@binaryfire

@binaryfire binaryfire commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

This completes the Reverb update to laravel/reverb main, following the first set of changes in #639. It also brings Scout up to laravel/scout 11.x, except for semantic and hybrid search (laravel/scout#1007, laravel/scout#1008, laravel/scout#1009 and laravel/scout#1012), which follow in their own PR. Hypervel already had most of Scout's other changes, so the Scout work is mainly upstream's missing tests, plus a fix for null search queries.

Upstream Updates

Reverb

  • laravel/reverb#406 closes the connection when a WebSocket handshake fails. Swoole already does this after any non-101 response to an upgrade request, and an invalid key already got a 400. But the WebSocket server never checked Sec-WebSocket-Version, so a client asking for an unsupported version was upgraded anyway. It now gets RFC 6455's 426 response with Sec-WebSocket-Version: 13, as Ratchet's negotiator sends for upstream. Upstream's two handshake tests are ported, and the handshake tests now send the version header that real clients send.
  • laravel/reverb#407 moved upstream's tests from Mockery to Double. Hypervel keeps Mockery. One client-event test expected a method the channel manager doesn't have never to be called, so it could never fail. It now checks that all() is never called, as upstream's does.
  • laravel/reverb@2f8a121 rejects signed HTTP API requests whose auth_timestamp is missing or more than 600 seconds from the current time. Hypervel accepted any timestamp, so a captured request could be replayed indefinitely. Upstream's test for expired and future timestamps is ported, along with a check that a correctly signed request without a timestamp is rejected.
  • laravel/reverb#408 fixed presence channels when Reverb scales across servers. Hypervel's design already covers most of it: member events travel as internal events that other servers don't cache, SharedState decides each user's first and last connection atomically across workers and servers, and presence members are gathered from every worker and server and merged. Two fixes are ported. A member without user_info made the subscription fail, and empty user_info was sent as []; both are now sent as {}. And if gathering members from other workers or servers failed after the subscription was committed, the client never got its subscription confirmation. The failure is now reported and the client gets this worker's members. Upstream's tests are ported, including its three Redis scaling tests.

Scout

  • laravel/scout#955 defaults the Typesense import action to upsert. Hypervel already did, but only the default was tested. Upstream's emplace test is ported, and the test helper's config stub, which returned the default for every key, now takes config values.
  • laravel/scout#962, laravel/scout#996 and laravel/scout#1002 made the Scout jobs' retries and backoff configurable, added opt-in unique indexing jobs and marked the jobs as failed on timeout by default. Hypervel already had all three, and upstream's missing job tests are ported. The unique ID test expects Hypervel's sha256 key, where upstream's expects md5.
  • laravel/scout#969 added where($field, $operator, $value), which Hypervel already supported. The collection and database engine tests for >, <, >=, <= and != are ported. The Meilisearch, Typesense and Algolia integration tests ran one combined query. They now run upstream's comparison cases with the changes from laravel/scout#976 and laravel/scout#978, which filter a typed numeric field and accept two results in either order. Typesense filters its integer ranking field, since its id is a string, and Algolia keeps its escaped-string case.
  • laravel/scout#1005 fixed boolean and inequality Algolia filters. Hypervel already had the fix, and its test gains upstream's boolean whereIn and whereNotIn cases.
  • laravel/scout#1011 fixed collection searches for "0", which Hypervel already handled. Upstream's new test also searches for null, which search() rejected: Model::search($request->query('q')) threw a TypeError when the request had no q. search() and the Builder constructor now accept null, and the builder stores it as an empty query. Upstream's null, whitespace and paginate-for-zero tests are ported.

Additional Hypervel Fixes

  • Scout's README listed internal changes, enhancements and fixes as differences from Laravel, and missed that pausing search syncing only applies to the current coroutine. It now lists only the differences that ported Laravel code has to account for, links the documentation and follows the standard package layout.
  • Three tests could fail depending on where in a second they ran, and two of them occasionally did in CI. The database and file cache stores report lock lifetimes in whole seconds, so the funnel lease refresh test could read the same lifetime before and after a refresh when a second ended between the reads. It now waits long enough for the refreshed lifetime to be longer. The duration limiter's windows end on a whole second, so the two tests that use a one-second window could open it at the very end of a second and let the immediate second attempt into a new window. They now start just after a whole second.

The changed tests, the Reverb unit and integration suites, the WebSocket server tests, the Scout unit and feature tests, the Meilisearch and Typesense filtering integration tests, formatting and static analysis pass locally. The Algolia integration tests weren't run locally because they need Algolia credentials, and CI runs them only when those credentials are configured.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Search queries can now be null; they’re treated as empty searches.
  • Bug Fixes
    • Presence subscriptions can still return channel members when metrics gathering fails, and missing member information is represented as an empty object.
    • Signed channel requests with missing or outdated timestamps are rejected.
    • WebSocket connections using unsupported protocol versions are rejected during the handshake.
  • Documentation
    • Updated Scout documentation to clarify package differences and usage requirements.

Note

Sync Reverb and Scout updates: presence fallback, signature timestamps, nullable Scout queries

  • Presence subscription responses now fall back to the current worker's unique members when the distributed metrics gather fails, while cancellation still propagates. Empty user info is serialized as an empty object in the presence hash (InteractsWithPresenceChannels.php).
  • Signed Reverb HTTP requests now also require a valid authentication timestamp within 600 seconds of the current time; invalid timestamps return HTTP 401 (Controller.php).
  • WebSocket handshakes requesting a protocol version other than 13 are rejected with HTTP 426 and upgrade headers, before routing (Server.php).
  • Scout search queries now accept null, normalized to an empty query in Builder and allowed by SearchableInterface (Builder.php).
  • Behavioral Change: existing signed Reverb HTTP clients without timestamps are now rejected with 401; WebSocket clients requesting versions below 13 get 426 instead of proceeding.

Macroscope summarized 0c706dc.

laravel/reverb PR 406 closes the connection when a WebSocket handshake
fails. Swoole already does this: it closes the connection after any
non-101 response to an upgrade request, and an invalid key already gets
a 400. The handshake never checked Sec-WebSocket-Version, though, so a
client asking for an unsupported version got a 101 instead of RFC
6455's 426. The websocket-server handshake now rejects it after the key
check with a 426 carrying Upgrade, Connection and Sec-WebSocket-Version
13, as Ratchet's negotiator does for upstream.

Upstream's two handshake tests are ported to the Reverb integration
ServerTest with their five-second client timeout, and ServerHandshakeTest
covers the rejection before routing. Handshake helpers in the tests now
send the version header a real client sends.

Upstream reference: laravel/reverb main at 74c8c4082c.

Validation: the changed test files, the WebSocketServer, Sentry,
Foundation HTTP and Reverb unit and integration suites, formatting and
PHPStan pass.
ClientEventTest's unsupported-message test expected
hydratedConnections() never to be called, but the channel connection
manager has no such method, so the expectation could never fail.
laravel/reverb PR 407 asserts all() is never called instead, and the
test now does the same.

PR 407 converts upstream's tests from Mockery to Double. Hypervel keeps
Mockery and doesn't port the exact call counts, which only constrain
internal lookups. It also keeps the all() and find() stubs that PR 407
removed from the members-mode and none tests: Double fails on unused
expectations, while Mockery's spy allows them, and the stubs are what
let those tests' assertNothingReceived() checks catch a forwarded
message.

Upstream reference: laravel/reverb main at 74c8c4082c.

Validation: the changed test file and the Reverb unit suite pass.
Signed HTTP API requests were accepted whatever their auth_timestamp,
so a captured request could be replayed indefinitely. Following
laravel/reverb commit 2f8a121813, the controller now rejects a request
whose timestamp is missing or more than 600 seconds from the current
time, after the signature itself is verified. Hypervel passes the
verified query to the check instead of reading it from controller
state.

The signed-request test helpers take upstream's optional timestamp.
ChannelsControllerTest ports the expired and future rejection test and
adds a correctly signed request without a timestamp.

Upstream reference: laravel/reverb main at 74c8c4082c.

Validation: the changed test files, the Reverb unit and integration
suites, formatting and PHPStan pass.
laravel/reverb PR 408 fixes presence channels when scaling is enabled.
Most of it is already part of Hypervel's design: member events are
routed as internal events, so other servers don't cache them;
SharedState atomically decides each user's first and last connection
across workers and servers; and presence data is gathered from every
worker or server and merged into one unique list. Upstream's
presence_connections metric, subscription timestamps and prefix-based
internal routing have no Hypervel counterpart.

Two fixes are ported into the subscription's presence data:

- A member without user_info made the subscription fail with an
  ErrorException, and a member whose user_info was {} was sent as [],
  because subscription data is decoded as an array. Both are now sent
  as {}, as upstream's merge does.
- A failed presence gather propagated after the subscription was
  committed, so the client never got its subscription_succeeded. The
  failure is now reported and the subscription is answered with this
  worker's members. Cancellation still propagates.

Tests ported: the no-user-info data test (with an empty user_info as
well), the merged and unknown-channel presence cases in
MetricsHandlerTest using Hypervel's snapshot payloads, the presence
cache test for internal events, the findOrCreate change, and the three
Redis scaling tests in RedisServerTest, with member_removed merged into
the existing member notification test. EventHandlerTest covers the
gather fallback and cancellation.

Upstream reference: laravel/reverb main at 74c8c4082c.

Validation: the changed test files, the Reverb unit and integration
suites, formatting and PHPStan pass.
Hypervel's Typesense engine already defaults the import action to
upsert and passes the configured action to the import, as laravel/scout
#955 does, but only the default was tested. The partial-engine test
helper now takes config values; its config stub previously returned the
default for every key. TypesenseEngineTest ports upstream's emplace
action test.

Upstream reference: laravel/scout 11.x at ce2542f5a7.

Validation: TypesenseEngineTest, formatting and PHPStan pass.
Hypervel's Scout jobs already read their retry, backoff and exception
limits from config, fail on timeout by default and support unique
indexing, but some of upstream's job tests were missing.

RemoveFromSearchTest ports the no-config, timeout default and timeout
opt-out tests from laravel/scout #962 and #1002, replacing timeout
assertions folded into the config tests. From #996 it ports the exact
unique ID test, adapted to Hypervel's sha256 key, and the
different-models test; the existing order test takes upstream's name.
MakeSearchableTest gains the different-models test.

Upstream reference: laravel/scout 11.x at ce2542f5a7.

Validation: both test files, formatting and PHPStan pass.
Hypervel's Builder and engines already support where() with a
comparison operator, as laravel/scout #969 added, but its per-operator
tests were missing. DatabaseEngineTest ports the >, <, >=, <= and !=
tests, and its existing same-field comparison test takes upstream's
name.

The Meilisearch, Typesense and Algolia filtering integration tests ran
one combined > and != query. They now run upstream's shared comparison
cases, including the fixes from #976 and #978, which filter a typed
numeric field and make the two-result assertions order-independent.
Typesense filters its int32 ranking field, since its id is a string,
and Algolia keeps its escaped-string case.

Upstream reference: laravel/scout 11.x at ce2542f5a7.

Validation: DatabaseEngineTest and the Meilisearch and Typesense
filtering integration tests pass against those services; the Algolia
integration test was not run locally because no Algolia credentials
are configured. Formatting and PHPStan pass.
laravel/scout #1011 tests that a collection search for null returns
every model. In Hypervel, search() and the Builder constructor only
accepted a string, so Model::search($request->query('q')) threw a
TypeError when the request had no q parameter. Both now accept a
nullable query, and the Builder stores null as an empty string so
engines keep receiving a string query.

The collection engine already treated only an empty string as an empty
query, so searches for "0" and whitespace worked. CollectionEngineTest
ports upstream's null, whitespace and paginate-for-zero tests, and the
existing zero test takes upstream's name. The same file also ports
#969's collection-engine >, <, >=, <= and != tests, and its existing
same-field comparison test takes upstream's name.

Upstream reference: laravel/scout 11.x at ce2542f5a7.

Validation: CollectionEngineTest, the Scout test suite, formatting and
PHPStan pass.
Hypervel's Algolia engine already compiles boolean and inequality
filters as laravel/scout #1005 does, but its test didn't cover boolean
values in whereIn() and whereNotIn(). The existing boolean and
inequality test now adds upstream's cases, asserting OR for inclusion
and AND for exclusion with boolean literals.

Upstream reference: laravel/scout 11.x at ce2542f5a7.

Validation: AlgoliaEngineTest, formatting and PHPStan pass.
The Scout README listed internal changes, enhancements and fixes as
differences, missed coroutine-local sync pausing, and didn't follow the
package README layout.

It now links the documentation and lists only the public differences
that ported Laravel code has to account for: Algolia 4 only, numeric
Algolia filter values, indexing without a queue, coroutine-local
search-sync pausing, the tenant token method's arguments and the
prefix requirement for deleting all indexes.

Upstream reference: laravel/scout 11.x at ce2542f5a7.
@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: hypervel/components/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: cca08a39-c1ed-4e5d-b62f-3f74b733a0eb

📥 Commits

Reviewing files that changed from the base of the PR and between e1dd9e1 and c6fdad0.

📒 Files selected for processing (28)
  • src/reverb/src/Protocols/Pusher/Channels/Concerns/InteractsWithPresenceChannels.php
  • src/reverb/src/Protocols/Pusher/Http/Controllers/Controller.php
  • src/scout/README.md
  • src/scout/src/Builder.php
  • src/scout/src/Contracts/SearchableInterface.php
  • src/scout/src/Searchable.php
  • src/websocket-server/src/Security.php
  • src/websocket-server/src/Server.php
  • tests/Integration/Reverb/RedisServerTest.php
  • tests/Integration/Reverb/ServerTest.php
  • tests/Integration/Scout/Algolia/AlgoliaFilteringIntegrationTest.php
  • tests/Integration/Scout/Meilisearch/MeilisearchFilteringIntegrationTest.php
  • tests/Integration/Scout/Typesense/TypesenseFilteringIntegrationTest.php
  • tests/Reverb/Protocols/Pusher/Channels/PresenceCacheChannelTest.php
  • tests/Reverb/Protocols/Pusher/Channels/PresenceChannelTest.php
  • tests/Reverb/Protocols/Pusher/ClientEventTest.php
  • tests/Reverb/Protocols/Pusher/EventHandlerTest.php
  • tests/Reverb/Protocols/Pusher/Http/Controllers/ChannelsControllerTest.php
  • tests/Reverb/Protocols/Pusher/MetricsHandlerTest.php
  • tests/Reverb/ReverbTestCase.php
  • tests/Scout/Feature/CollectionEngineTest.php
  • tests/Scout/Feature/DatabaseEngineTest.php
  • tests/Scout/Unit/Engines/AlgoliaEngineTest.php
  • tests/Scout/Unit/Engines/TypesenseEngineTest.php
  • tests/Scout/Unit/Jobs/MakeSearchableTest.php
  • tests/Scout/Unit/Jobs/RemoveFromSearchTest.php
  • tests/Sentry/WebSocketRuntimeContextTest.php
  • tests/WebSocketServer/ServerHandshakeTest.php

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The pull request updates Reverb presence snapshots and signed-request timestamp checks, adds WebSocket version validation, and changes Scout query typing and documentation. It also expands tests for these behaviors and for search-engine filters and job identifiers.

Changes

Reverb presence handling

Layer / File(s) Summary
Presence snapshot fallback and serialization
src/reverb/src/Protocols/Pusher/Channels/Concerns/InteractsWithPresenceChannels.php, tests/Reverb/Protocols/Pusher/Channels/PresenceChannelTest.php
Presence data rethrows cancellation exceptions. For other metrics failures, it reports the error and uses deduplicated local channel members. Falsy user information is represented as an empty object.
Metrics merge and subscription failure handling
tests/Reverb/Protocols/Pusher/MetricsHandlerTest.php, tests/Reverb/Protocols/Pusher/EventHandlerTest.php
Tests cover merged presence snapshots, absent channels, and subscription responses or exception propagation when metrics gathering fails.
Presence scaling and cache behavior
tests/Integration/Reverb/RedisServerTest.php, tests/Reverb/Protocols/Pusher/Channels/PresenceCacheChannelTest.php, tests/Reverb/Protocols/Pusher/ClientEventTest.php
Tests cover scaled member snapshots and notifications, cache behavior for internal presence events, and the updated all() expectation.

Reverb request timestamp validation

Layer / File(s) Summary
Timestamp validation and request tests
src/reverb/src/Protocols/Pusher/Http/Controllers/Controller.php, tests/Reverb/Protocols/Pusher/Http/Controllers/ChannelsControllerTest.php, tests/Reverb/ReverbTestCase.php
Signature verification rejects missing or non-numeric timestamps and timestamps more than 600 seconds from the current time. Test helpers accept explicit timestamps; tests cover expired, future, and missing values.

WebSocket handshake version validation

Layer / File(s) Summary
Handshake version contract and rejection
src/websocket-server/src/Security.php, src/websocket-server/src/Server.php, tests/Integration/Reverb/ServerTest.php, tests/WebSocketServer/ServerHandshakeTest.php, tests/Sentry/WebSocketRuntimeContextTest.php
The server rejects unsupported WebSocket versions with HTTP 426 and upgrade-related headers. Tests cover unsupported versions and provide the supported version in handshake fixtures.

Scout query and engine behavior

Layer / File(s) Summary
Nullable queries and collection search
src/scout/src/Builder.php, src/scout/src/Contracts/SearchableInterface.php, src/scout/src/Searchable.php, src/scout/README.md, tests/Scout/Feature/CollectionEngineTest.php
Search accepts nullable queries, and the builder stores an empty string for null. Collection tests cover query, comparison-filter, and pagination cases. The README revises its list of differences from Laravel Scout.
Comparison filters across search engines
tests/Scout/Feature/DatabaseEngineTest.php, tests/Integration/Scout/Algolia/AlgoliaFilteringIntegrationTest.php, tests/Integration/Scout/Meilisearch/MeilisearchFilteringIntegrationTest.php, tests/Integration/Scout/Typesense/TypesenseFilteringIntegrationTest.php, tests/Scout/Unit/Engines/AlgoliaEngineTest.php
Tests cover comparison operators and combined ranges across search engines, plus Boolean inclusion and exclusion filters in Algolia.
Engine configuration and job behavior tests
tests/Scout/Unit/Engines/TypesenseEngineTest.php, tests/Scout/Unit/Jobs/MakeSearchableTest.php, tests/Scout/Unit/Jobs/RemoveFromSearchTest.php
Tests cover the Typesense emplace import action and job configuration and unique-identifier behavior.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to c6fda

No concrete issue remains that calls for a fix before merging; the change is ready subject to normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to c6fda

Request validation becomes stricter, and no new security bypass was identified. Presence responses can now be incomplete during outages, while compatibility with application-specific callers and recovery behavior remains partly unverified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • observed — Anonymous clients can exercise the WebSocket protocol-validation gate on listeners using this server. Signed HTTP actions remain bound to the resolved application secret, and degraded presence data remains scoped to the authorized application and channel. The inspected changes do not grant a new identity or privilege.

Trust Boundaries and Controls

  • observed — Signed HTTP requests retain method, path, query, actual-body hash, and application-secret HMAC validation. Freshness is checked only after signature acceptance. Missing, non-numeric, stale, or excessively future timestamps are rejected before verified request context reaches the inspected controller actions.
  • observed — The new version check rejects unsupported or missing versions before route and middleware dispatch, without replacing the existing key check. Rejection uses HTTP 426 and advertises version 13. The rejection path releases unpublished connection state; the focused test asserts no routing, no established-connection check, and no retained connection context.

Resilience and Maintainability Implications

  • observed — The fallback is a read-only response mechanism over stored members. It deduplicates user IDs and uses contained failure reporting, so an ordinary reporting exception cannot prevent the degraded response. It does not reconcile distributed membership or establish global snapshot completeness.

Hardening Proposals

  • proposed — Document the worker-local degraded presence contract and keep presence counts and snapshots separate from authoritative authorization decisions. This addresses possible consumer misuse; it is not an observed vulnerability.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.45% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 88 functions across 27 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description provides detailed change scope, supporting tests, and verification results, but it does not complete the required contribution-type and submission checkboxes or list exact verification… Select the applicable contribution type, complete all required checklist items, list the exact commands and results including the required composer fix run, and submit the work through the repository’s missing upstream functionality process…
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely identifies the main Reverb and Scout synchronization changes.
Full details: Docstring Coverage

Explanation

Docstring coverage is 20.45% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 88 functions across 27 files. (1 skipped: 1 unsupported.)

Full details: Description check

Explanation

The description provides detailed change scope, supporting tests, and verification results, but it does not complete the required contribution-type and submission checkboxes or list exact verification commands. It also explicitly describes the pull request as a porting or synchronization PR, which the repository template says not to open.

Resolution

Select the applicable contribution type, complete all required checklist items, list the exact commands and results including the required composer fix run, and submit the work through the repository’s missing upstream functionality process if it is a porting or synchronization change.

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@binaryfire

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@binaryfire

Copy link
Copy Markdown
Member Author

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 2, 2026

Copy link
Copy Markdown

@cubic-dev-ai review

@binaryfire I have started the AI code review. It will take a few minutes to complete.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Sync Reverb and Scout behavior with upstream releases

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Reject unsupported WebSocket versions and stale signed Reverb requests to enforce protocol and
 replay protections.
• Keep committed presence subscriptions responsive when member gathering fails; accept null Scout
 searches.
• Port upstream presence, filtering, and job tests; clarify Scout’s Hypervel-specific behavior.
Diagram

graph TD
  Client["WebSocket client"] --> WS["WebSocket server"] --> Presence["Presence channel"] --> Metrics["Member gathering"]
  Client --> API["Reverb HTTP API"] --> Signature["Timestamp validation"]
  Caller["Search caller"] --> Builder["Scout builder"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Fail closed on presence gathering errors
  • ➕ Never return an incomplete cross-server member snapshot.
  • ➖ Leaves an already-committed subscription without confirmation when gathering fails.

Recommendation: Keep the local-member fallback: it confirms a committed subscription and reports the gathering failure, while coroutine cancellation still propagates. Reviewers should accept that the confirmation may temporarily omit remote members.

Files changed (28) +636 / -82

Bug fix (7) +67 / -13
InteractsWithPresenceChannels.phpPreserve presence confirmations when member gathering fails +27/-6

Preserve presence confirmations when member gathering fails

• Reports gathering errors and builds a local-member snapshot instead of abandoning an already-committed subscription; coroutine cancellation still propagates. Serializes missing or empty user information as an empty object.

src/reverb/src/Protocols/Pusher/Channels/Concerns/InteractsWithPresenceChannels.php

Controller.phpExpire signed Reverb API requests after ten minutes +21/-0

Expire signed Reverb API requests after ten minutes

• Rejects signed requests with a missing, nonnumeric, expired, or excessively future auth timestamp, limiting replay of captured requests.

src/reverb/src/Protocols/Pusher/Http/Controllers/Controller.php

Builder.phpNormalize null search queries to empty strings +2/-2

Normalize null search queries to empty strings

• Accepts a nullable constructor query and stores null as an empty query.

src/scout/src/Builder.php

SearchableInterface.phpAllow nullable queries in the searchable contract +1/-1

Allow nullable queries in the searchable contract

• Changes the search method contract to accept null, matching the supported public API.

src/scout/src/Contracts/SearchableInterface.php

Searchable.phpAccept null in model search calls +1/-1

Accept null in model search calls

• Permits callers to pass a nullable query to Searchable::search without a type error.

src/scout/src/Searchable.php

Security.phpName the WebSocket version header +2/-0

Name the WebSocket version header

• Adds a header constant used by handshake validation and tests.

src/websocket-server/src/Security.php

Server.phpReject unsupported WebSocket versions before routing +13/-3

Reject unsupported WebSocket versions before routing

• Requires version 13 after key validation and responds to unsupported versions with HTTP 426 and upgrade headers.

src/websocket-server/src/Server.php

Tests (20) +561 / -60
RedisServerTest.phpCover presence behavior with Redis scaling +62/-0

Cover presence behavior with Redis scaling

• Tests member removal, existing members in subscription confirmations, and the absence of cached internal member events across scaled servers.

tests/Integration/Reverb/RedisServerTest.php

ServerTest.phpExercise invalid WebSocket handshakes end to end +36/-0

Exercise invalid WebSocket handshakes end to end

• Checks that invalid keys receive HTTP 400 and unsupported versions receive HTTP 426.

tests/Integration/Reverb/ServerTest.php

AlgoliaFilteringIntegrationTest.phpExpand Algolia comparison-filter integration coverage +13/-13

Expand Algolia comparison-filter integration coverage

• Tests all five comparison operators and combined ranges against numeric IDs, while retaining escaped-string coverage.

tests/Integration/Scout/Algolia/AlgoliaFilteringIntegrationTest.php

MeilisearchFilteringIntegrationTest.phpExpand Meilisearch comparison-filter integration coverage +12/-8

Expand Meilisearch comparison-filter integration coverage

• Replaces one combined filter case with individual comparison and range checks, allowing either order when both models match.

tests/Integration/Scout/Meilisearch/MeilisearchFilteringIntegrationTest.php

TypesenseFilteringIntegrationTest.phpExpand Typesense numeric-filter integration coverage +12/-8

Expand Typesense numeric-filter integration coverage

• Tests comparison operators and ranges against the typed numeric ranking field rather than the string ID field.

tests/Integration/Scout/Typesense/TypesenseFilteringIntegrationTest.php

PresenceCacheChannelTest.phpVerify internal presence events are not cached +35/-1

Verify internal presence events are not cached

• Exercises subscription, remotely dispatched member addition, and unsubscription on a registered presence-cache channel, asserting none caches an internal payload.

tests/Reverb/Protocols/Pusher/Channels/PresenceCacheChannelTest.php

PresenceChannelTest.phpCheck absent presence user information +20/-0

Check absent presence user information

• Verifies that missing and empty user information both serialize as empty JSON objects in subscription data.

tests/Reverb/Protocols/Pusher/Channels/PresenceChannelTest.php

ClientEventTest.phpMake the unsupported-event expectation effective +1/-1

Make the unsupported-event expectation effective

• Asserts that unsupported client events never call the channel manager’s real all() method, replacing an expectation for a nonexistent method.

tests/Reverb/Protocols/Pusher/ClientEventTest.php

EventHandlerTest.phpTest presence gathering failure and cancellation +59/-0

Test presence gathering failure and cancellation

• Checks that ordinary gathering failures are reported and answered with local members, while coroutine cancellation propagates without a response.

tests/Reverb/Protocols/Pusher/EventHandlerTest.php

ChannelsControllerTest.phpTest signed-request timestamp rejection +30/-0

Test signed-request timestamp rejection

• Covers expired and future signatures and a correctly signed request with no timestamp.

tests/Reverb/Protocols/Pusher/Http/Controllers/ChannelsControllerTest.php

MetricsHandlerTest.phpStrengthen distributed presence snapshot assertions +28/-8

Strengthen distributed presence snapshot assertions

• Checks deduplication across numeric and string user IDs, preservation of empty user information, and the result when no server has the channel.

tests/Reverb/Protocols/Pusher/MetricsHandlerTest.php

ReverbTestCase.phpSupport explicit timestamps in signed-request helpers +6/-3

Support explicit timestamps in signed-request helpers

• Lets GET and POST test helpers sign requests with a supplied timestamp while retaining the current-time default.

tests/Reverb/ReverbTestCase.php

CollectionEngineTest.phpCover collection filtering and unusual search queries +79/-2

Cover collection filtering and unusual search queries

• Adds comparison-operator cases, null and whitespace searches, and pagination for the string query "0".

tests/Scout/Feature/CollectionEngineTest.php

DatabaseEngineTest.phpCover database-engine comparison operators +52/-1

Cover database-engine comparison operators

• Adds separate cases for greater-than, less-than, inclusive comparisons, and inequality alongside the existing combined-filter case.

tests/Scout/Feature/DatabaseEngineTest.php

AlgoliaEngineTest.phpAssert boolean Algolia set-filter compilation +4/-2

Assert boolean Algolia set-filter compilation

• Extends the expected filter expression to cover boolean whereIn and whereNotIn values.

tests/Scout/Unit/Engines/AlgoliaEngineTest.php

TypesenseEngineTest.phpTest configured Typesense emplace imports +25/-8

Test configured Typesense emplace imports

• Checks the emplace import action and makes the partial-engine test helper return supplied configuration values.

tests/Scout/Unit/Engines/TypesenseEngineTest.php

MakeSearchableTest.phpCheck unique indexing IDs differ across model sets +8/-0

Check unique indexing IDs differ across model sets

• Adds an assertion that distinct sets of searchable models produce distinct unique job IDs.

tests/Scout/Unit/Jobs/MakeSearchableTest.php

RemoveFromSearchTest.phpExpand removal-job configuration and uniqueness tests +45/-4

Expand removal-job configuration and uniqueness tests

• Separately verifies unset retry properties, timeout defaults and overrides, and sha256-based unique IDs that are stable across model order but differ across model sets.

tests/Scout/Unit/Jobs/RemoveFromSearchTest.php

WebSocketRuntimeContextTest.phpSend a supported version in Sentry handshake fixtures +1/-0

Send a supported version in Sentry handshake fixtures

• Adds the version 13 header so runtime-context tests exercise valid WebSocket handshakes.

tests/Sentry/WebSocketRuntimeContextTest.php

ServerHandshakeTest.phpVerify unsupported versions fail before routing +33/-1

Verify unsupported versions fail before routing

• Asserts the HTTP 426 response headers, absence of routing, and cleanup of connection context; handshake fixtures now provide a configurable version header.

tests/WebSocketServer/ServerHandshakeTest.php

Documentation (1) +8 / -9
README.mdClarify Scout’s documented differences from Laravel +8/-9

Clarify Scout’s documented differences from Laravel

• Links Hypervel’s Scout documentation and narrows the differences list to port-relevant behavior, including coroutine-local search-sync pauses.

src/scout/README.md

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Models that override search fail to load 🐞 Bug ☼ Reliability
Description
SearchableInterface::search() and Searchable::search() widen $query from string to
?string, so any model override still declared with string $query is now an incompatible
declaration. PHP raises that as a fatal error when the class loads, breaking every request that
touches the model; separately, custom $scoutBuilder classes with a string $query constructor
throw a TypeError on search(null).
Code

src/scout/src/Contracts/SearchableInterface.php[30]

+    public static function search(?string $query = '', ?Closure $callback = null): Builder;
Evidence
The interface parameter is now ?string. PHP requires an implementing method's parameter types to
be the same or wider, so an existing string override becomes a fatal incompatible declaration.
Searchable::search passes the raw $query to Container::makeWith for a custom $scoutBuilder,
so search(null) reaches that builder's constructor as null.

src/scout/src/Contracts/SearchableInterface.php[30-30]
src/scout/src/Searchable.php[243-254]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `search()` query parameter changed from `string` to `?string` in both the interface and the trait. Existing model overrides declared `public static function search(string $query = '', ...)` no longer match the interface, which is a fatal error at class load. Custom builder classes passed `null` via `$scoutBuilder` may also throw a TypeError.
## Fix Focus Areas
- src/scout/src/Contracts/SearchableInterface.php[30-30]
- src/scout/src/Searchable.php[243-254]
- src/scout/README.md[10-16]
## Recommended Fix
Pick one option.
Option A: keep the nullable signature, normalise `$query ?? ''` in `Searchable::search()` before passing it to `makeWith` so custom builders always receive a string, and add a README / upgrade note telling users to widen `search()` overrides to `?string`.
Option B: keep the interface at `string` and convert null to `''` in the trait instead.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/scout/src/Contracts/SearchableInterface.php

@cubic-dev-ai cubic-dev-ai 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.

5 issues found across 28 files

Confidence score: 3/5

  • Existing SearchableInterface implementations that keep string $query can fail to load after this signature change. Add an upgrade note telling implementers to widen the parameter to ?string.
  • In SearchableInterface, search(null) can pass null to custom builders that require a string and throw a TypeError. Normalize the query to '' before resolving the builder.
  • The differences section in src/scout/README.md no longer mentions the removeAllFromSearch() force-flag difference, so readers may miss that this method still differs from Laravel’s parameterless version. Restore that note.
  • The Meilisearch note in src/scout/README.md leaves out the search rules parameter to generateTenantToken(), which could mislead readers about the method signature.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="tests/Integration/Reverb/ServerTest.php">

<violation number="1" location="tests/Integration/Reverb/ServerTest.php:75">
P3: This test verifies only the 426 status code and not the `Sec-WebSocket-Version` response header, which RFC 6455 §4.2.2 requires on a version-rejection handshake and which the server does send (src/websocket-server/src/Server.php throws the 426 with `Sec-WebSocket-Version => Security::VERSION`). Since the PR highlights the RFC 6455 426 behavior, assert the header (plus the `Upgrade: websocket`) so a regression that drops it is caught.</violation>
</file>

<file name="src/scout/README.md">

<violation number="1" location="src/scout/README.md:8">
P3: The differences rewrite drops the documented `Searchable::removeAllFromSearch()` force-flag difference, but the method still deviates from Laravel's parameterless `removeAllFromSearch()`: `src/scout/src/Searchable.php:316` is `removeAllFromSearch(bool $force = false)`, and the `scout:flush` command enables it via `Scout::guardModelFlush`. That is a deliberate lasting public-contract difference that the README should keep (or point to the docs section covering it) so porters don't miss the force behavior.</violation>

<violation number="2" location="src/scout/README.md:14">
P3: This bullet describes the Meilisearch client method imprecisely: it says the client's `generateTenantToken()` "takes the UID first and an options array," but the signature also takes the search rules as the second positional argument (`$uid, $searchRules, $options`), and the engine still passes the call through to the client at the end (after validating and reordering arguments). Tighten the wording so the contrast is accurate.</violation>
</file>

<file name="src/scout/src/Contracts/SearchableInterface.php">

<violation number="1" location="src/scout/src/Contracts/SearchableInterface.php:30">
P2: Normalize `$query` to `''` before resolving `$scoutBuilder`; `search(null)` otherwise passes null to custom builders whose constructor requires `string` and throws a `TypeError`.</violation>

<violation number="2" location="src/scout/src/Contracts/SearchableInterface.php:30">
P2: Existing `SearchableInterface` implementations with `string $query` now fail class loading. Add an upgrade note requiring those implementations to widen the parameter to `?string`.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

* @return Builder<Model&static>
*/
public static function search(string $query = '', ?Closure $callback = null): Builder;
public static function search(?string $query = '', ?Closure $callback = null): Builder;

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: Normalize $query to '' before resolving $scoutBuilder; search(null) otherwise passes null to custom builders whose constructor requires string and throws a TypeError.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/scout/src/Contracts/SearchableInterface.php, line 30:

<comment>Normalize `$query` to `''` before resolving `$scoutBuilder`; `search(null)` otherwise passes null to custom builders whose constructor requires `string` and throws a `TypeError`.</comment>

<file context>
@@ -27,7 +27,7 @@ interface SearchableInterface
      * @return Builder<Model&static>
      */
-    public static function search(string $query = '', ?Closure $callback = null): Builder;
+    public static function search(?string $query = '', ?Closure $callback = null): Builder;
 
     /**
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Keeping this as is. Laravel's search() also passes the raw query to static::$scoutBuilder. Since search() returns a Builder, a custom builder is a Builder subclass, and Builder's constructor accepts null and stores it as an empty query. Normalizing the query in search() as well would duplicate that.

* @return Builder<Model&static>
*/
public static function search(string $query = '', ?Closure $callback = null): Builder;
public static function search(?string $query = '', ?Closure $callback = null): Builder;

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: Existing SearchableInterface implementations with string $query now fail class loading. Add an upgrade note requiring those implementations to widen the parameter to ?string.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/scout/src/Contracts/SearchableInterface.php, line 30:

<comment>Existing `SearchableInterface` implementations with `string $query` now fail class loading. Add an upgrade note requiring those implementations to widen the parameter to `?string`.</comment>

<file context>
@@ -27,7 +27,7 @@ interface SearchableInterface
      * @return Builder<Model&static>
      */
-    public static function search(string $query = '', ?Closure $callback = null): Builder;
+    public static function search(?string $query = '', ?Closure $callback = null): Builder;
 
     /**
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

No note needed. Laravel's search($query = '', $callback = null) has no parameter type, so overrides ported from Laravel don't declare string $query, and an untyped $query is compatible with ?string. Only an override copied from Hypervel's earlier signature is affected, and the upgrade guide already recommends moving 0.3 code into a fresh 0.4 application and updating it to the Laravel-style APIs rather than listing individual signature changes.

]);
$client->get('/app/' . $this->appKey);

$this->assertSame(426, $client->getStatusCode());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This test verifies only the 426 status code and not the Sec-WebSocket-Version response header, which RFC 6455 §4.2.2 requires on a version-rejection handshake and which the server does send (src/websocket-server/src/Server.php throws the 426 with Sec-WebSocket-Version => Security::VERSION). Since the PR highlights the RFC 6455 426 behavior, assert the header (plus the Upgrade: websocket) so a regression that drops it is caught.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/Integration/Reverb/ServerTest.php, line 75:

<comment>This test verifies only the 426 status code and not the `Sec-WebSocket-Version` response header, which RFC 6455 §4.2.2 requires on a version-rejection handshake and which the server does send (src/websocket-server/src/Server.php throws the 426 with `Sec-WebSocket-Version => Security::VERSION`). Since the PR highlights the RFC 6455 426 behavior, assert the header (plus the `Upgrade: websocket`) so a regression that drops it is caught.</comment>

<file context>
@@ -41,6 +43,40 @@ public function testFailsToConnectWithInvalidAppKey(): void
+        ]);
+        $client->get('/app/' . $this->appKey);
+
+        $this->assertSame(426, $client->getStatusCode());
+
+        $client->close();
</file context>
Suggested change
$this->assertSame(426, $client->getStatusCode());
$this->assertSame(426, $client->getStatusCode());
$this->assertSame('13', $client->getHeaders()['sec-websocket-version'] ?? null);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 4f1f7aa. The live test now also asserts Sec-WebSocket-Version: 13. A running Reverb server renders handshake errors through the application's exception handler, while the unit tests cover the WebSocket server's own handler, so this checks the header on the production path.

I didn't add a separate Upgrade assertion. The server sets Upgrade, Connection and Sec-WebSocket-Version together on the same exception, ServerHandshakeTest already asserts all three, and the version header check shows those headers reach the client through the application's handler.

Comment thread src/scout/README.md Outdated
- `Searchable::removeAllFromSearch()` accepts an optional force flag, which the explicit `scout:flush` command enables.
- Without a queue, indexing is deferred until after the HTTP response is sent, and runs immediately outside a request.
- Pausing search syncing with `withoutSyncingToSearch()` or `disableSearchSyncing()` applies only to the current coroutine, so other requests keep indexing. See [Pausing Indexing](https://hypervel.org/docs/scout#pausing-indexing).
- `MeilisearchEngine::generateTenantToken()` takes the search rules, the parent key's UID and the key itself, instead of passing the call through to the Meilisearch client, whose method takes the UID first and an options array. See [Tenant Tokens](https://hypervel.org/docs/scout#meilisearch-tenant-tokens).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This bullet describes the Meilisearch client method imprecisely: it says the client's generateTenantToken() "takes the UID first and an options array," but the signature also takes the search rules as the second positional argument ($uid, $searchRules, $options), and the engine still passes the call through to the client at the end (after validating and reordering arguments). Tighten the wording so the contrast is accurate.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/scout/README.md, line 14:

<comment>This bullet describes the Meilisearch client method imprecisely: it says the client's `generateTenantToken()` "takes the UID first and an options array," but the signature also takes the search rules as the second positional argument (`$uid, $searchRules, $options`), and the engine still passes the call through to the client at the end (after validating and reordering arguments). Tighten the wording so the contrast is accurate.</comment>

<file context>
@@ -3,16 +3,15 @@ Scout for Hypervel
-- `Searchable::removeAllFromSearch()` accepts an optional force flag, which the explicit `scout:flush` command enables.
+- Without a queue, indexing is deferred until after the HTTP response is sent, and runs immediately outside a request.
+- Pausing search syncing with `withoutSyncingToSearch()` or `disableSearchSyncing()` applies only to the current coroutine, so other requests keep indexing. See [Pausing Indexing](https://hypervel.org/docs/scout#pausing-indexing).
+- `MeilisearchEngine::generateTenantToken()` takes the search rules, the parent key's UID and the key itself, instead of passing the call through to the Meilisearch client, whose method takes the UID first and an options array. See [Tenant Tokens](https://hypervel.org/docs/scout#meilisearch-tenant-tokens).
+- `scout:delete-all-indexes` refuses to run without a configured Scout prefix unless you pass `--force`.
+
</file context>
Suggested change
- `MeilisearchEngine::generateTenantToken()` takes the search rules, the parent key's UID and the key itself, instead of passing the call through to the Meilisearch client, whose method takes the UID first and an options array. See [Tenant Tokens](https://hypervel.org/docs/scout#meilisearch-tenant-tokens).
+`MeilisearchEngine::generateTenantToken()` takes the search rules, the parent key's UID and the key itself, and validates and reorders them before delegating to the Meilisearch client's `generateTenantToken()`, which takes the UID first followed by the search rules and an options array. See [Tenant Tokens](https://hypervel.org/docs/scout#meilisearch-tenant-tokens).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 0c706dc. The entry now describes both methods accurately: Hypervel's engine takes the search rules, the parent key's UID, the key and an optional expiry, and calls the client with them in the client's order. Laravel's engine forwards the call unchanged to the Meilisearch client, whose method takes the UID, the search rules and an options array.

Comment thread src/scout/README.md

Differences From Laravel
---
## Differences From Laravel

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The differences rewrite drops the documented Searchable::removeAllFromSearch() force-flag difference, but the method still deviates from Laravel's parameterless removeAllFromSearch(): src/scout/src/Searchable.php:316 is removeAllFromSearch(bool $force = false), and the scout:flush command enables it via Scout::guardModelFlush. That is a deliberate lasting public-contract difference that the README should keep (or point to the docs section covering it) so porters don't miss the force behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/scout/README.md, line 8:

<comment>The differences rewrite drops the documented `Searchable::removeAllFromSearch()` force-flag difference, but the method still deviates from Laravel's parameterless `removeAllFromSearch()`: `src/scout/src/Searchable.php:316` is `removeAllFromSearch(bool $force = false)`, and the `scout:flush` command enables it via `Scout::guardModelFlush`. That is a deliberate lasting public-contract difference that the README should keep (or point to the docs section covering it) so porters don't miss the force behavior.</comment>

<file context>
@@ -3,16 +3,15 @@ Scout for Hypervel
 
-Differences From Laravel
----
+## Differences From Laravel
 
 - Algolia 4 is the only supported Algolia client.
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Keeping this out of the README. Its differences section only lists differences that ported Laravel code has to account for. removeAllFromSearch() still works with no arguments, as in Laravel. The optional force argument only matters to an application that registers a model-flush guard, and the Scout documentation covers it under Removing Records and Customizing Scout Lifecycles.

Two timing assertions occasionally failed in the Redis Cluster CI runs.

The funnel lease refresh test slept 1.1 seconds after acquiring a
three-second lease, then expected the refreshed lifetime to be longer
than the remaining one. The database and file stores report lifetimes
in whole seconds, rounding the deadline up and the current time down,
so when a second ended between the two reads both values could be 3.
Sleeping 2.1 seconds leaves at most 2 seconds before the refresh and at
least 3 after it.

The duration limiter's window runs from the current whole second to
that second plus the decay, while each attempt is compared with the
fractional current time. A one-second window opened late in a second
only lasts for the rest of that second, so an immediate second attempt
could land in a new window and succeed. This is normal fixed-window
behavior, shared with Laravel's limiter, so the limiter is unchanged.
The two tests that expect an immediate second attempt in a one-second
window to fail now start just after a whole second.

Validation: DurationLimiterIntegrationTest and the cache funnel tests
pass against Redis.
The live Reverb server test for an unsupported WebSocket version only
checked the 426 status. A running Reverb server renders handshake
errors through the application's exception handler, while the unit
tests that check the Sec-WebSocket-Version header cover the WebSocket
server's own handler. The live test now also asserts the
Sec-WebSocket-Version: 13 header that RFC 6455 requires on this
response, so the production path is covered.

Upstream reference: laravel/reverb main at 74c8c4082c.

Validation: ServerTest passes against a Reverb test server.
The entry left out the search rules when describing the Meilisearch
client's generateTenantToken() and didn't mention the engine's optional
expiry. It now lists the arguments of both methods: Hypervel's engine
takes the search rules, the parent key's UID, the key and an optional
expiry, while Laravel's engine forwards the call to the client, whose
method takes the UID, the search rules and an options array.

Upstream reference: laravel/scout 11.x at ce2542f5a7.
@binaryfire
binaryfire merged commit 709defb into 0.4 Oct 2, 2026
53 of 54 checks passed
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.

1 participant