Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d024a0a
Require evidence before departing from Laravel API compatibility
binaryfire Oct 31, 2026
903695b
Fix touching related models through intermediate relations
binaryfire Oct 31, 2026
a30aa91
Batch Redis bulk queue payloads and notifications together
binaryfire Oct 31, 2026
d6434c7
Reconcile scheduled callback exception regression coverage
binaryfire Oct 31, 2026
4946921
Cover unique queue locks after rollback and failed dispatch
binaryfire Oct 31, 2026
a842bac
Support broadcast uniqueness attributes and synchronous Mercure
binaryfire Oct 31, 2026
1040ef6
Preserve fractional CarbonInterval process timeouts
binaryfire Oct 31, 2026
1e7d32f
Keep reconnect recursion guards active for leased drivers
binaryfire Oct 31, 2026
6f5a729
Skip contextual binding lookup when no bindings exist
binaryfire Oct 31, 2026
3e47fd4
Apply model casts to extra increment and decrement attributes
binaryfire Oct 31, 2026
6834834
Observe scheduler overlap locks without acquiring them
binaryfire Oct 31, 2026
42d0927
Honor the complete blocking deadline in locks and limiters
binaryfire Oct 31, 2026
90c3e22
Constrain both schema tables when inspecting MySQL foreign keys
binaryfire Oct 31, 2026
3b1fe4b
Preserve self aliases in one-of-many existence queries
binaryfire Oct 31, 2026
46f896e
Include elapsed runtime in failed scheduled task events
binaryfire Oct 31, 2026
2357596
Preserve JSON:API loading customization with batched default includes
binaryfire Oct 31, 2026
b31dcec
Qualify the deleted column when restoring joined models
binaryfire Oct 31, 2026
8678ac5
Support path-based MIME detection and Swoole FTP installations
binaryfire Oct 31, 2026
84c44ff
Allow queue:forget to remove multiple failed jobs
binaryfire Oct 31, 2026
2c8fac8
Qualify implicit timestamp ordering on joined Eloquent queries
binaryfire Oct 31, 2026
54e1673
Prefer upstream fixes when reconciling equivalent Hypervel code
binaryfire Oct 31, 2026
667fd79
Preserve broadcast guarantees across synchronous and deferred dispatch
binaryfire Oct 31, 2026
751c9e2
Preserve relationship scopes and key bindings when touching related m…
binaryfire Oct 31, 2026
441a7eb
Keep one-of-many subqueries independent of outer model aliases
binaryfire Oct 31, 2026
3503956
Synchronize written increment values through model change hooks
binaryfire Oct 31, 2026
8cade85
Keep lock inspection and release tied to the current owner
binaryfire Oct 31, 2026
3a103d8
Require FTP adapters that support path-based MIME detection
binaryfire Oct 31, 2026
db07ca4
Clarify JSON API collection loader ownership
binaryfire Oct 31, 2026
4c2556b
Avoid repeated array reindexing in string replacement
binaryfire Oct 31, 2026
7b72bd7
Remove an unused exception binding from the reconnect regression
binaryfire Oct 31, 2026
5617484
Reject refreshes through replaced file-lock handles
binaryfire Oct 31, 2026
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
3 changes: 3 additions & 0 deletions .github/docker/ci/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ RUN set -eux; \

RUN printf '%s\n' 'swoole.use_shortname=Off' > /usr/local/etc/php/conf.d/99-swoole-shortname.ini

# The development Composer root declares the FTP capability supplied by Swoole.
RUN php -r 'if (! function_exists("ftp_connect") || extension_loaded("ftp")) { fwrite(STDERR, "Swoole FTP support without the separate FTP extension is required for filesystem tests.\n"); exit(1); }'

RUN set -eux; \
test "$(php -r 'echo PHP_MAJOR_VERSION, ".", PHP_MINOR_VERSION;')" = "${PHP_VERSION}"; \
test "$(php -r 'echo phpversion("swoole");')" = "${SWOOLE_VERSION}"; \
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -794,9 +794,9 @@ When porting Laravel packages, whether first-party or third-party, keep them as
- Not porting deprecated upstream code or backwards-compatibility shims for versions/features Hypervel does not support — Hypervel is a new framework without Laravel's backwards-compatibility burden, so deprecated APIs and compatibility code that exist only to support older versions should be omitted rather than ported. Before changing or removing a deprecated public Laravel API, reach consensus on the recommended approach through a second-opinion loop with your peer agent. Then STOP, explain the proposed difference and its rationale to the owner, and wait for approval. Here, "upstream" means the framework or package being ported, not one of its dependencies — a Symfony deprecation does not make a Laravel API deprecated while Laravel still retains it. If a deprecated upstream surface still contains behavior that Hypervel actively needs, keep the behavior but move it onto the correct non-deprecated Hypervel-owned surface instead of porting the deprecated alias/wrapper as-is.
- General performance improvements — reach consensus on the expected benefit, correctness, and complexity tradeoffs through a second-opinion loop with your peer agent before implementing.

Hypervel has no obligation to preserve Hypervel-specific behavior from earlier versions, but supported Laravel APIs—including named arguments and protected extension points—must remain compatible unless the user approves a difference. If a Laravel API is unsuitable for Hypervel or preserving it would make the code worse, reach consensus on the cleanest design through a second-opinion loop with your peer agent. Then STOP, explain the proposed difference and its rationale to the owner, and wait for approval before changing it.
Hypervel has no obligation to preserve Hypervel-specific behavior from earlier versions, but supported Laravel APIs—including named arguments and protected extension points—must remain compatible unless the user approves a difference. Compatibility means preserving supported behavior, not merely matching names and signatures. Before recommending a departure, investigate the simplest compatible adaptation and reasonable alternatives, and establish the concrete obstacle. Resolve uncertainty through focused tests or prototypes; an assumed architectural conflict or peer agreement is not sufficient evidence. If preserving compatibility would compromise correctness, performance or scalability, or require disproportionate complexity, reach consensus on the cleanest design through a second-opinion loop with your peer agent. Then STOP, explain the proposed difference, supporting evidence and rationale to the owner, and wait for approval before changing it.

When Laravel or a tracked upstream introduces an equivalent to a Hypervel-specific enhancement, compare their capabilities, APIs, coroutine safety and performance, then reach consensus on how to reconcile them through a second-opinion loop with your peer agent. Normally prefer the upstream API when it meets Hypervel’s requirements. Before deprecating or removing the Hypervel-specific API, STOP, explain the recommendation to the owner, and wait for approval.
When Laravel or a tracked upstream introduces an equivalent to a Hypervel-specific enhancement or fix, compare their capabilities, APIs, coroutine safety and performance, then reach consensus on how to reconcile them through a second-opinion loop with your peer agent. Normally prefer upstream when it meets Hypervel’s requirements. Before deprecating or removing the Hypervel-specific API, STOP, explain the recommendation to the owner, and wait for approval.

Approved adaptations take precedence over upstream fidelity. Preserve Laravel upstream naming, structure, and style everywhere else.

Expand Down
9 changes: 8 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@
"fakerphp/faker": "^1.24",
"friendsofphp/php-cs-fixer": "^3.57.2",
"intervention/image": "^4.2",
"league/flysystem-ftp": "^3.36",
"league/flysystem-google-cloud-storage": "^3.0",
"league/flysystem-sftp-v3": "^3.36",
"meilisearch/meilisearch-php": "^1.16",
"mockery/mockery": "^1.6.15",
"opis/json-schema": "^2.6.0",
Expand All @@ -145,6 +147,9 @@
"typesense/typesense-php": "^5.2",
"web-token/jwt-library": "^4.2.3"
},
"provide": {
"ext-ftp": "*"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
},
Comment on lines +150 to +152

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

5. Monorepo package claims ftp for every consumer 🐞 Bug ☼ Reliability

The root composer.json (hypervel/components, type library) now declares `"provide":
{"ext-ftp": "*"}. Composer honours provide` on installed dependencies, so any project that
requires hypervel/components has its ext-ftp requirement satisfied even when its Swoole build
lacks --enable-swoole-ftp. Such a project can install league/flysystem-ftp and only fails at
runtime when ftp_connect is undefined, which contradicts the PR's claim that published packages do
not claim the extension.
Agent Prompt
## Issue description
`provide: ext-ftp` in the root `hypervel/components` manifest is inherited by every project that installs that package, which hides a missing Swoole FTP build until runtime.

## Fix Focus Areas
- composer.json[150-152]

## Recommended Fix
Remove `provide.ext-ftp` from the root manifest. To satisfy the dev-only adapter, use a mechanism that only applies at the development root, such as `config.platform.ext-ftp` or `--ignore-platform-req=ext-ftp` in CI. The Dockerfile `function_exists('ftp_connect')` check stays as the real capability gate. If the root must keep it, confirm `hypervel/components` is never installed as a dependency and document that.

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

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.

This is the development-only monorepo root. Supported applications install the split packages, whose manifests do not provide ext-ftp. The application-root declaration is documented alongside the required Swoole build and native-extension conflict. CI verifies that the development runtime exposes ftp_connect with the native ftp extension absent.

"replace": {
"hypervel/ai": "self.version",
"hypervel/api-client": "self.version",
Expand Down Expand Up @@ -235,7 +240,9 @@
},
"conflict": {
"aws/aws-sdk-php": "<3.369.1",
"intervention/image": "<4.0 || >=5.0"
"intervention/image": "<4.0 || >=5.0",
"league/flysystem-ftp": "<3.13",
"league/flysystem-sftp-v3": "<3.13"
},
"minimum-stability": "dev",
"prefer-stable": true,
Expand Down
4 changes: 0 additions & 4 deletions docs/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,6 @@
- Add native return types to the `test*` methods that lack them: about 5,600 methods in 490 files. `AGENTS.md` requires `: void` on test methods; a test that returns a value for `#[Depends]` takes its actual return type instead. As with method titles, untyped methods in existing files lead new tests to copy them, so make the change as one approved sweep and enforce it with the same automated check.
- Integrate the resolution of [PHPUnit #7039](https://github.com/sebastianbergmann/phpunit/issues/7039) into `RunTestsInCoroutine` once PHPUnit 13.5 is released ([scheduled for December 4, 2026](https://github.com/sebastianbergmann/phpunit/blob/main/ChangeLog-13.5.md)); it adds `TestCase::suspendOutputBuffering()` and `resumeOutputBuffering()` ([PR #7040](https://github.com/sebastianbergmann/phpunit/pull/7040)). Follow the merged upstream design, including calling `parent::invokeTestMethod()` if capture is handled there. Add regression coverage for output expectations, flushed output and buffer cleanup; update the testing guidance and reassess output-related coroutine opt-outs. Lift the `~13.4.3` PHPUnit constraints in the root, Testbench and dogfood manifests, the testing package's suggestion and the `hypervel/hypervel` skeleton once Testbench's PHPUnit integration is verified on 13.5. Do not add a private-buffer adapter, maintained fork or ongoing Composer/vendor patches. Prefer supported subclass or method overrides; use AOP only as an absolute last resort. Never document a confirmed defect as an accepted limitation.

## Filesystem

- Investigate FTP support with Swoole's built-in coroutine FTP implementation. It supplies `ftp_*` functions but is not discoverable as `ext-ftp`, so Composer rejects `league/flysystem-ftp` and `RequiresPhpExtension('ftp')` skips the driver test. Resolve normal development and production installation without bypassing dependency checks, then add the adapter to root `require-dev`, use a test requirement that accepts either FTP implementation, and update the installation guidance. Basic transfers through Hypervel and Flysystem have been verified inside a Swoole coroutine.

## AI and MCP

- Revisit conversation-history query plans when production evidence or newer database versions warrant it; this does not block the AI port. Reproduce with the stock migration, one oldest conversation containing 10,000 messages, then 999 other participants/conversations with 20 newer messages each. After updating statistics, inspect actual execution of `DatabaseConversationStore::latestConversationId()`, `getLatestConversationMessages(..., 100)` and the first/subsequent cursor pages. MySQL 9.5 and PostgreSQL chose reverse primary-key scans, examining 20,080 rows for 100 messages despite `(conversation_id, id)` being available; smaller targets at roughly 1% of the table used conversation indexes and did not scan other conversations. MariaDB 10.11 read history efficiently but filtered cursor IDs after seeking only the conversation, scanning preceding pages within it; SQLite 3.45 used both index bounds. The query shape matches Laravel's. Selecting IDs first, including through a bounded subquery joined back to messages, did not fix the MySQL/PostgreSQL plans. Include the similar latest-row queries in `pendingApprovalsFor()` and `pausedRowFor()` when investigating; their execution plans have not been measured. Test an index-derived scalar upper bound on message IDs with both oldest-only and interleaved conversations. A stored pointer needs maintenance across all supported writes; a stale low pointer would hide messages. Keep current queries and indexes unless a measured improvement preserves other workloads, configured/partitioned schemas and pagination behavior without unnecessary round trips or database-specific machinery.
Expand Down
1 change: 1 addition & 0 deletions skeletons/hypervel/config/broadcasting.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
],
'cookie_name' => env('MERCURE_COOKIE_NAME'),
'subscribe_expiration' => (float) env('MERCURE_SUBSCRIBE_EXPIRATION', 5),
'async' => (bool) env('MERCURE_ASYNC', false),
],

'redis' => [
Expand Down
1 change: 1 addition & 0 deletions skeletons/react-starter-kit/config/broadcasting.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
],
'cookie_name' => env('MERCURE_COOKIE_NAME'),
'subscribe_expiration' => (float) env('MERCURE_SUBSCRIBE_EXPIRATION', 5),
'async' => (bool) env('MERCURE_ASYNC', false),
],

'redis' => [
Expand Down
16 changes: 16 additions & 0 deletions src/broadcasting/src/BroadcastEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Hypervel\Bus\Queueable;
use Hypervel\Contracts\Broadcasting\Factory as BroadcastingFactory;
use Hypervel\Contracts\Broadcasting\ShouldRescue;
use Hypervel\Contracts\Queue\ShouldQueue;
use Hypervel\Contracts\Queue\ShouldQueueViaOutbox;
use Hypervel\Contracts\Support\Arrayable;
Expand Down Expand Up @@ -203,6 +204,21 @@ public function failed(?Throwable $e = null): void
$this->event->failed($e);
}

/**
* Handle a deferred dispatch or transaction cleanup failure.
*
* @internal
*/
public function deferredDispatchFailed(Throwable $exception): void
{
if (! $this->event instanceof ShouldRescue) {
throw $exception;
}

// The manager's rescue callback has returned before deferred queue work runs.
rescue(static fn () => throw $exception);
}

/**
* Get the display name for the queued job.
*/
Expand Down
70 changes: 60 additions & 10 deletions src/broadcasting/src/BroadcastManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
use Hypervel\Contracts\ObjectPool\Factory as PoolFactory;
use Hypervel\Contracts\ObjectPool\InvalidatesPool;
use Hypervel\Contracts\Queue\Factory as Queue;
use Hypervel\Contracts\Queue\ShouldQueueViaOutbox;
use Hypervel\Contracts\Redis\Factory as RedisFactory;
use Hypervel\Database\DatabaseTransactionsManager;
use Hypervel\Foundation\Http\Middleware\PreventRequestForgery;
use Hypervel\Http\Client\Guzzle\HandlerFactory;
use Hypervel\Http\Request;
Expand Down Expand Up @@ -199,9 +201,13 @@ public function event(mixed $event = null): PendingBroadcast
*/
public function queue(mixed $event): void
{
if ($event instanceof ShouldBroadcastNow
|| (is_object($event) && method_exists($event, 'shouldBroadcastNow') && $event->shouldBroadcastNow())
) {
$immediate = $event instanceof ShouldBroadcastNow
|| (is_object($event) && method_exists($event, 'shouldBroadcastNow') && $event->shouldBroadcastNow());
$synchronousMercure = ! $immediate
&& ! ($event->viaOutbox ?? $event instanceof ShouldQueueViaOutbox)
&& $this->broadcastsToSynchronousMercureConnections($event);

if ($immediate || ($synchronousMercure && ! $event instanceof ShouldBeUnique && empty($event->afterCommit))) {
$dispatch = fn () => $this->app->make(Dispatcher::class)->dispatchNow(
new BroadcastEvent($event instanceof UnitEnum ? $event : clone $event)
);
Expand Down Expand Up @@ -233,13 +239,15 @@ public function queue(mixed $event): void
return;
}

$push = fn () => $this->app->make(Queue::class)
->connection(
$event->connection
?? $this->getAttributeValue($event, ConnectionAttribute::class, 'connection')
?? $this->resolveConnectionFromQueueRoute($event, $queue)
)
->pushOn($queue, $broadcastEvent);
$push = $synchronousMercure
? fn () => $this->dispatchSynchronousBroadcast($broadcastEvent)
: fn () => $this->app->make(Queue::class)
->connection(
$event->connection
?? $this->getAttributeValue($event, ConnectionAttribute::class, 'connection')
?? $this->resolveConnectionFromQueueRoute($event, $queue)
)
->pushOn($queue, $broadcastEvent);

if ($event instanceof ShouldBeUnique) {
$dispatch = $push;
Expand All @@ -257,6 +265,48 @@ public function queue(mixed $event): void
: $push();
}

/**
* Publish synchronously while honoring commit timing and dispatch locks.
*/
protected function dispatchSynchronousBroadcast(BroadcastEvent $event): void
{
$dispatch = fn () => $this->app->make(Dispatcher::class)->dispatchNow($event);

if ($event->afterCommit && $this->app->bound('db.transactions')) {
/** @var DatabaseTransactionsManager $transactions */
$transactions = $this->app->make('db.transactions');

if ($transactions->hasApplicableTransaction()) {
// Direct publishing retains the event without queue serialization or model reloads.
$release = static function () use ($event): void {
DispatchLockContext::claim($event);
DispatchLockContext::release($event);
};
$transactions->addCallbackForRollback(fn () => $event->event instanceof ShouldRescue
? $this->rescue($release)
: $release());

$publish = static function () use ($event, $dispatch): void {
DispatchLockContext::claim($event);

try {
$dispatch();
} finally {
DispatchLockContext::release($event);
}
};
$transactions->addCallback(fn () => $event->event instanceof ShouldRescue
? $this->rescue($publish)
: $publish());
DispatchLockContext::delegate($event);

return;
}
}

$dispatch();
}

/**
* Get the container that owns the queue routes.
*/
Expand Down
25 changes: 25 additions & 0 deletions src/broadcasting/src/Mercure/CreatesMercureDrivers.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
use Symfony\Component\Mercure\Jwt\WebTokenFactory;
use Symfony\Contracts\HttpClient\HttpClientInterface;

use function Hypervel\Support\enum_value;

trait CreatesMercureDrivers
{
protected const string DEFAULT_MERCURE_ALGORITHM = 'HS256';
Expand Down Expand Up @@ -105,6 +107,29 @@ public function mercure(array $config): HubInterface

// REMOVED: frankenPhpMercure() calls FrankenPHP's in-process hub; Hypervel uses Swoole and publishes over HTTP.

/**
* Determine if the given event only targets Mercure connections publishing synchronously.
*/
protected function broadcastsToSynchronousMercureConnections(mixed $event): bool
{
$connections = method_exists($event, 'broadcastConnections')
? $event->broadcastConnections()
: [null];

foreach ($connections as $connection) {
$connection = enum_value($connection);
$config = $this->getConfig($connection === null || $connection === ''
? $this->getDefaultDriver()
: (string) $connection);

if (($config['driver'] ?? null) !== 'mercure' || ! empty($config['async'])) {
return false;
}
}

return $connections !== [];
}

/**
* Identify the shared HTTP-client pool for construction and invalidation.
*/
Expand Down
5 changes: 3 additions & 2 deletions src/broadcasting/src/UniqueBroadcastEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Hypervel\Container\Container;
use Hypervel\Contracts\Cache\Repository;
use Hypervel\Contracts\Queue\ShouldBeUnique;
use Hypervel\Queue\Attributes\UniqueFor;

class UniqueBroadcastEvent extends BroadcastEvent implements ShouldBeUnique
{
Expand All @@ -33,8 +34,8 @@ public function __construct(mixed $event)

if (method_exists($event, 'uniqueFor')) {
$this->uniqueFor = $event->uniqueFor();
} elseif (property_exists($event, 'uniqueFor')) {
$this->uniqueFor = $event->uniqueFor;
} else {
$this->uniqueFor = $this->getAttributeValue($event, UniqueFor::class, 'uniqueFor', $this->uniqueFor);
}

parent::__construct($event);
Expand Down
2 changes: 2 additions & 0 deletions src/cache/src/DatabaseLock.php
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ public function pruneExpiredLocks(): void
protected function getCurrentOwner(): ?string
{
return $this->connection()->table($this->table)
->useWritePdo()
->where('key', $this->name)
->where('expiration', '>', $this->currentTime())
->first()
Expand Down Expand Up @@ -266,6 +267,7 @@ public function getRemainingLifetime(): ?float
$now = $this->currentTime();

$lock = $this->connection()->table($this->table)
->useWritePdo()
->where('key', $this->name)
->where('expiration', '>', $now)
->first();
Expand Down
16 changes: 16 additions & 0 deletions src/cache/src/FileLock.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@ public function acquire(): bool
return $this->fileStore()->add($this->name, $this->owner, $this->seconds);
}

/**
* Release the lock without deleting another owner's replacement.
*/
public function release(): bool
{
return $this->fileStore()->forgetIfOwned($this->name, $this->owner);
}

/**
* Return the current owner without deleting an expired lock record.
*/
protected function getCurrentOwner(): ?string
{
return $this->fileStore()->getLockOwner($this->name);
}

/**
* Refresh the lock's TTL if still owned by this process.
*
Expand Down
Loading
Loading