Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f549271
Add Markdown responses and preserve streaming cancellation
binaryfire Oct 28, 2026
b5b5246
Preserve chained cancellation through HTTP and AI streaming
binaryfire Oct 28, 2026
c7108a6
Support lazy creation of local filesystem roots
binaryfire Oct 28, 2026
f921b87
Allow all named scheduled tasks to run on one server
binaryfire Oct 28, 2026
9452b92
Verify HTTP pending requests release their default throw callback
binaryfire Oct 28, 2026
4d0f3f3
Reconcile QUERY route coverage
binaryfire Oct 28, 2026
4751ab5
Reconcile fractional duration helper tests
binaryfire Oct 28, 2026
8dbd7c7
Release unique job and listener locks after scoped queue fakes
binaryfire Oct 28, 2026
7c9bcb6
Preserve pending attributes when force-creating related models
binaryfire Oct 28, 2026
7e37ce2
Correct one-of-many and polymorphic relationship comparisons
binaryfire Oct 28, 2026
bd4b2d8
Cover null passwords in session cookie hashing
binaryfire Oct 28, 2026
4e569a4
Preserve raw binary cursors when paging cast model keys
binaryfire Oct 28, 2026
049e5d4
Touch custom polymorphic pivot owners before detaching
binaryfire Oct 28, 2026
3d47d9b
test(collections): cover mixed eager and lazy collection values
binaryfire Oct 28, 2026
44eb8eb
fix(database): normalize enum default connection names
binaryfire Oct 28, 2026
a05d116
fix(object-pool): preserve cancellation through throwing cleanup
binaryfire Oct 28, 2026
bc27228
fix(queue): preserve cancellation across SQS dispatch and cleanup
binaryfire Oct 28, 2026
b736724
fix(queue): propagate database dispatch and recovery cancellation
binaryfire Oct 28, 2026
b10b063
fix(queue): retain cancellation through Redis batch publication
binaryfire Oct 28, 2026
6267289
fix(queue): stop outbox staging when cleanup carries cancellation
binaryfire Oct 28, 2026
2210ce6
fix(queue): propagate cancellation from sequential outbox delivery
binaryfire Oct 28, 2026
9fee215
fix(queue): preserve cancellation across outbox relay phases
binaryfire Oct 28, 2026
a91186c
fix(queue): retain cancellation during SQS job deletion cleanup
binaryfire Oct 28, 2026
c96fcdc
fix(queue): avoid requeueing after canceled lease attachment
binaryfire Oct 28, 2026
9c23d3c
docs: align package README section ordering
binaryfire Oct 28, 2026
7cfcfa1
Merge current 0.4 into upstream sync branch
binaryfire Oct 28, 2026
c7e6a06
Release only dispatch locks owned by scoped queue fakes
binaryfire Oct 28, 2026
cbf7b54
Preserve model values and binary binding intent during pagination
binaryfire Oct 28, 2026
1752506
Set relationship keys before forced creation mutators
binaryfire Oct 28, 2026
3359ca4
Reject unrelated models before reading relationship keys
binaryfire Oct 28, 2026
3763d45
Preserve cancellation during queue outbox selection
binaryfire Oct 28, 2026
49d2c8e
Clarify scheduler defaults during provider boot
binaryfire Oct 28, 2026
2d4e195
Resolve binary paging casts through the query source alias
binaryfire Oct 28, 2026
f287863
Retain scoped fake lock ownership when the facade is replaced
binaryfire Oct 28, 2026
3507b43
Preserve explicit stream values in binary cursor bindings
binaryfire Oct 28, 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
8 changes: 5 additions & 3 deletions src/ai/src/Streaming/Protocols/StreamProtocol.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
use Generator;
use Hypervel\Ai\Exceptions\StreamErrorException;
use Hypervel\Ai\Responses\StreamableAgentResponse;
use Hypervel\Coroutine\Coroutine;
use Hypervel\Http\IterableStreamedResponse;
use Swoole\Coroutine\CanceledException;
use Symfony\Component\HttpFoundation\Response;
use Throwable;

Expand Down Expand Up @@ -54,9 +54,11 @@ public function response(StreamableAgentResponse $response): Response
foreach ($this->parts($response) as $part) {
yield $this->encode($part);
}
} catch (CanceledException $e) {
throw $e;
} catch (Throwable $e) {
if (Coroutine::causedByCancellation($e)) {
throw $e;
}

$this->exception = $e;

// A stream error exception carries a provider error the stream already surfaced, so only report anything else...
Expand Down
4 changes: 2 additions & 2 deletions src/collections/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ Collections for Hypervel

Documentation: https://hypervel.org/docs/collections

Ported from: https://github.com/laravel/framework/tree/13.x/src/Illuminate/Collections

## Differences From Laravel

Laravel's deprecated `containsOneItem()` and `containsManyItems()` aliases are intentionally not ported. Use `hasSole()` and `hasMany()` instead.

`Enumerable::random()` exposes the optional `preserveKeys` argument supported by both concrete collection implementations and reports the preserved key type accurately. Laravel exposes this argument only on the concrete classes and always reports integer keys through the contract.

`Arr::push()` accepts arrays only. Laravel also advertises `ArrayAccess`, but dot notation requires nested by-reference mutation that `ArrayAccess` cannot provide.

Ported from: https://github.com/laravel/framework/tree/13.x/src/Illuminate/Collections
4 changes: 2 additions & 2 deletions src/concurrency/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ Concurrency for Hypervel

Documentation: https://hypervel.org/docs/concurrency

Ported from: https://github.com/laravel/framework/tree/13.x/src/Illuminate/Concurrency

## Differences From Laravel

Hypervel uses its Swoole coroutine driver by default; the process and sync drivers remain available explicitly. Laravel's fork driver is omitted because coroutine concurrency is Hypervel's native lightweight execution model.

Ported from: https://github.com/laravel/framework/tree/13.x/src/Illuminate/Concurrency
29 changes: 28 additions & 1 deletion src/console/src/Scheduling/Schedule.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ class Schedule
*/
public static bool $interruptible = true;

/**
* Indicates if every event should only run on one server for each cron expression.
*/
public static bool $alwaysOnOneServer = false;

/**
* Create a new schedule instance.
*
Expand Down Expand Up @@ -394,7 +399,7 @@ public function dueEvents(Application $app): Collection
*/
public function dueEventsAt(Application $app, DateTimeInterface $time): Collection
{
return (new Collection($this->events))
return (new Collection($this->events()))
->filter(fn (Event $event) => $event->isDueAt($app, $time));
}

Expand All @@ -405,6 +410,16 @@ public function dueEventsAt(Application $app, DateTimeInterface $time): Collecti
*/
public function events(): array
{
if (static::$alwaysOnOneServer) {
foreach ($this->events as $event) {
if ($event instanceof CallbackEvent && $event->description === null) {
continue;
}

$event->onOneServer();
}
}

return $this->events;
}

Expand Down Expand Up @@ -497,13 +512,25 @@ public function hasBeenInterruptedSince(DateTimeInterface $time): bool
return is_numeric($interruptedAt) && (int) $interruptedAt >= Date::instance($time)->getTimestampMs();
}

/**
* Indicate that every event on the schedule should only run on one server for each cron expression.
*
* Boot-only. The flag persists for the worker lifetime and affects every
* schedule evaluated in the process.
*/
public static function alwaysOnOneServer(bool $value = true): void
{
static::$alwaysOnOneServer = $value;
}

/**
* Flush all static state.
*/
public static function flushState(): void
{
static::$pausable = true;
static::$interruptible = true;
static::$alwaysOnOneServer = false;
static::flushMacros();
}

Expand Down
7 changes: 6 additions & 1 deletion src/contracts/src/Routing/ResponseFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ public function json(mixed $data = [], int $status = 200, array $headers = [], i
*/
public function jsonp(string $callback, mixed $data = [], int $status = 200, array $headers = [], int $options = 0): JsonResponse;

/**
* Create a new Markdown response instance.
*/
public function markdown(string $content = '', int $status = 200, array $headers = []): Response;

/**
* Create a new event stream response.
*
Expand All @@ -53,7 +58,7 @@ public function eventStream(Closure $callback, array $headers = [], StreamedEven
/**
* Create a new streamed response instance.
*/
public function stream(callable $callback, int $status = 200, array $headers = []): StreamedResponse;
public function stream(?callable $callback, int $status = 200, array $headers = []): StreamedResponse;

/**
* Create a new streamed JSON response instance.
Expand Down
13 changes: 13 additions & 0 deletions src/database/src/BinaryParameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ public function __construct(public string $value)
{
}

/**
* Create a binary parameter from a stream and rewind it.
*
* @param resource $stream
*/
public static function fromStream(mixed $stream): static
{
$value = stream_get_contents($stream, offset: 0);
rewind($stream);

return new static($value);
}

/**
* Return the binary parameter value.
*/
Expand Down
89 changes: 78 additions & 11 deletions src/database/src/Concerns/BuildsQueries.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Generator;
use Hypervel\Container\Container;
use Hypervel\Database\Eloquent\Builder;
use Hypervel\Database\Eloquent\Model;
use Hypervel\Database\MultipleRecordsFoundException;
use Hypervel\Database\Query\Builder as QueryBuilder;
use Hypervel\Database\Query\Expression;
Expand Down Expand Up @@ -205,7 +206,7 @@ public function orderedChunkById(int $count, callable $callback, ?string $column
return false;
}

$lastId = data_get($results->last(), $alias);
$lastId = $this->getNextPageIdFromChunk($results->last(), $column, $alias);

if ($lastId === null) {
throw new RuntimeException("The chunkById operation was aborted because the [{$alias}] column is not present in the query result.");
Expand Down Expand Up @@ -283,6 +284,8 @@ public function lazy(int $chunkSize = 1000): LazyCollection
return;
}

unset($results);

++$page;
}
});
Expand Down Expand Up @@ -366,17 +369,72 @@ protected function orderedLazyById(int $chunkSize = 1000, ?string $column = null
return;
}

$lastId = data_get($results->last(), $alias);
$lastId = $this->getNextPageIdFromChunk($results->last(), $column, $alias);

if ($lastId === null) {
throw new RuntimeException("The lazyById operation was aborted because the [{$alias}] column is not present in the query result.");
}

unset($results);

++$page;
}
});
}

/**
* Get the ID of the last result of a chunk to continue paging from.
*/
protected function getLastIdFromChunk(mixed $result, string $alias): mixed
{
if (! $result instanceof Model) {
return data_get($result, $alias);
}

// Callbacks and accessors can provide cursor aliases that are not selected columns.
return array_key_exists($alias, $original = $result->getRawOriginal())
? $original[$alias]
: data_get($result, $alias);
}
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.

/**
* Prepare the last chunk ID for comparison with the paging column.
*/
protected function getNextPageIdFromChunk(mixed $result, string $column, string $alias): mixed
{
$lastId = $this->getLastIdFromChunk($result, $alias);

if (! $result instanceof Model) {
return $lastId;
}

// Either the selected alias or the model's paging column can declare binary binding intent.
$attribute = $this->getPagingAttribute($result, $column);
$lastId = $result->prepareBinaryAttributesForDatabase([$alias => $lastId])[$alias];

return $result->prepareBinaryAttributesForDatabase([$attribute => $lastId])[$attribute];
}

/**
* Get the model attribute behind a qualified paging column.
*/
protected function getPagingAttribute(Model $model, string $column): string
{
if ($model->hasCast($column)) {
return $column;
}

$prefixes = [$model->getTable() . '.'];

$query = $this instanceof Builder ? $this->getQuery() : $this;

if (($alias = $query->getFromAlias()) !== null) {
$prefixes[] = $alias . '.';
}

return Str::chopStart($column, $prefixes);
}

/**
* Execute the query and get the first result.
*
Expand Down Expand Up @@ -454,42 +512,43 @@ protected function paginateUsingCursor(
// Reset the union bindings so we can add the cursor where in the correct position...
$this->setBindings([], 'union');

$addCursorConditions = function (self $builder, $previousColumn, $originalColumn, $i) use (&$addCursorConditions, $cursor, $orders) {
$unionBuilders = $builder->getUnionBuilders();
$addCursorConditions = function (QueryBuilder|Builder $builder, $previousColumn, $originalColumn, $i) use (&$addCursorConditions, $cursor, $orders) {
// Unions are base queries; the other builder family only appears in nested constraints without unions.
$unionBuilders = $builder instanceof self ? $builder->getUnionBuilders() : new Collection;

if (! is_null($previousColumn)) {
$originalColumn ??= $this->getOriginalColumnNameForCursorPagination($this, $previousColumn);

$builder->where(
Str::contains($originalColumn, ['(', ')']) ? new Expression($originalColumn) : $originalColumn,
'=',
$cursor->parameter($previousColumn)
$this->getCursorParameterBinding($cursor, $previousColumn)
);

$unionBuilders->each(function ($unionBuilder) use ($previousColumn, $cursor) {
$unionBuilder->where(
$this->getOriginalColumnNameForCursorPagination($unionBuilder, $previousColumn),
'=',
$cursor->parameter($previousColumn)
$this->getCursorParameterBinding($cursor, $previousColumn)
);

$this->addBinding($unionBuilder->getRawBindings()['where'], 'union');
});
}

$builder->where(function (self $secondBuilder) use ($addCursorConditions, $cursor, $orders, $i, $unionBuilders) {
$builder->where(function (QueryBuilder|Builder $secondBuilder) use ($addCursorConditions, $cursor, $orders, $i, $unionBuilders) {
['column' => $column, 'direction' => $direction] = $orders[$i];

$originalColumn = $this->getOriginalColumnNameForCursorPagination($this, $column);

$secondBuilder->where(
Str::contains($originalColumn, ['(', ')']) ? new Expression($originalColumn) : $originalColumn,
$direction === 'asc' ? '>' : '<',
$cursor->parameter($column)
$this->getCursorParameterBinding($cursor, $column)
);

if ($i < $orders->count() - 1) {
$secondBuilder->orWhere(function (self $thirdBuilder) use ($addCursorConditions, $column, $originalColumn, $i) {
$secondBuilder->orWhere(function (QueryBuilder|Builder $thirdBuilder) use ($addCursorConditions, $column, $originalColumn, $i) {
$addCursorConditions($thirdBuilder, $column, $originalColumn, $i + 1);
});
}
Expand All @@ -502,11 +561,11 @@ protected function paginateUsingCursor(
$unionBuilder->where(
$originalColumn,
$direction === 'asc' ? '>' : '<',
$cursor->parameter($column)
$this->getCursorParameterBinding($cursor, $column)
);

if ($i < $orders->count() - 1) {
$unionBuilder->orWhere(function (self $fourthBuilder) use ($addCursorConditions, $column, $originalColumn, $i) {
$unionBuilder->orWhere(function (QueryBuilder|Builder $fourthBuilder) use ($addCursorConditions, $column, $originalColumn, $i) {
$addCursorConditions($fourthBuilder, $column, $originalColumn, $i + 1);
});
}
Expand All @@ -530,6 +589,14 @@ protected function paginateUsingCursor(
]);
}

/**
* Get a cursor parameter for database binding.
*/
protected function getCursorParameterBinding(Cursor $cursor, string $parameter): mixed
{
return $cursor->parameter($parameter);
}

/**
* Get the original column name of the given column, without any aliasing.
*
Expand Down
6 changes: 5 additions & 1 deletion src/database/src/DatabaseManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -571,8 +571,12 @@ public function allowQueryDurationHandlersToRunAgain(): void
* worker are not affected. A null value clears the override and
* getDefaultConnection() falls back to config('database.default').
*/
public function setDefaultConnection(?string $name): void
public function setDefaultConnection(UnitEnum|string|null $name): void
{
if ($name instanceof UnitEnum) {
$name = (string) enum_value($name);
}

if ($name === null) {
CoroutineContext::forget(ConnectionResolver::DEFAULT_CONNECTION_CONTEXT_KEY);
} else {
Expand Down
13 changes: 12 additions & 1 deletion src/database/src/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,17 @@ protected function ensureOrderForCursorPagination(bool $shouldReverse = false):
->values();
}

/**
* Get a cursor parameter for database binding.
*/
protected function getCursorParameterBinding(Cursor $cursor, string $parameter): mixed
{
// Reverse the cast that produced the readable cursor, preserving qualified pivot casts.
$key = $this->getPagingAttribute($this->model, $parameter);

return $this->model->encodeBinaryAttributeForDatabase($key, $cursor->parameter($parameter));
}

/**
* Save a new model and return the instance.
*
Expand Down Expand Up @@ -1216,7 +1227,7 @@ public function createQuietly(array $attributes = []): Model
public function forceCreate(array $attributes): Model
{
return $this->model::unguarded(function () use ($attributes) {
return $this->newModelInstance()->create($attributes);
return $this->create($attributes);
});
}

Expand Down
Loading
Loading