Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Hypervel is a standalone Laravel-style Swoole framework. The public API should s

Laravel is the main API reference. Hyperf is a historical and architectural reference for some lower-level Swoole/coroutine packages, but Hypervel code should follow current Hypervel patterns rather than copying Hyperf structure mechanically.

Most work in this repo today is framework bug fixing and enhancement, or porting Laravel packages. Hyperf-to-Hypervel porting is largely done — the conversion guide lives in `docs/ai/porting-hyperf.md`, read when maintaining previously ported code or doing the occasional remaining port.
Most work in this repo today is framework bug fixes and enhancements, or porting Laravel packages and updates. `docs/ai/porting-hyperf.md` applies only to the rare Hyperf package or update port.

This file is intentionally detailed because agents trained on Laravel will otherwise assume Laravel's request lifecycle and miss Hypervel's Swoole/coroutine constraints.

Expand Down Expand Up @@ -125,6 +125,7 @@ The Working rules and the Avoid overengineering rules apply to all work in this
- **One file at a time** — never work on multiple files simultaneously. This governs manual editing; package-manager and formatter runs may touch multiple files.
- **Never use Write to overwrite files** — always use Edit for targeted updates.
- **Always use `cp` to copy files and `mv` to move/rename** — never read → write new version → delete old version.
- **Copy before splitting** — When a new file or class is primarily extracted from existing code, use `cp` to copy the primary source first and then update the copy rather than rebuilding it from individual pieces. Copy any additional blocks, including comments and docblocks, into the destination before removing them from their source.
- **Grep broadly — never assume a subdir** — when searching for any symbol, class, method, or pattern, grep across the whole `src/` (or `tests/`) tree, not a specific package subdir. Assumptions about where something lives produce false negatives.
- **Read the source before describing behavior** — never state how code behaves from memory or Laravel assumptions. Hypervel's coroutine runtime breaks many Laravel assumptions; if you haven't read the relevant source, read it first.
- **Treat past owner decisions as context, not constraints** — Previous owner approvals and completed plans explain history but do not determine the best design today. Never retain or reject a design merely because it was previously approved; decide from current requirements, code, and evidence.
Expand Down Expand Up @@ -758,7 +759,7 @@ Hyperf is a historical reference rather than an ongoing merge target. For the ra

When working on a package, check its README for the upstream reference before making changes. Most Hypervel packages are ports of Laravel first-party or third-party ecosystem packages, such as Spatie packages. Most low-level Swoole infrastructure packages were originally ported from Hyperf, and a few packages are Hypervel-specific.

Before porting Hyperf code or modifying a Hyperf-ported package, read `docs/ai/porting-hyperf.md` — it covers the conversion mechanics: container calls, ConfigProvider migration, listener/event conversion, and Hyperf test porting.
Read `docs/ai/porting-hyperf.md` only when porting a Hyperf package or update.

### Source workflow

Expand Down
2 changes: 1 addition & 1 deletion docs/ai/porting-hyperf.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Porting Hyperf Code to Hypervel

Read this before porting Hyperf code or modifying a Hyperf-ported package. It covers the Hyperf side of the conversion: container calls, ConfigProviders, listeners/events, and tests. Hypervel's own container semantics, binding patterns, and alias rules live in the Container section of `AGENTS.md` — this doc assumes you have read them.
Read this guide only when porting a Hyperf package or update. It covers the Hyperf side of the conversion: container calls, ConfigProviders, listeners/events, and tests. Hypervel's own container semantics, binding patterns, and alias rules live in the Container section of `AGENTS.md` — this doc assumes you have read them.

Hyperf ports do not aim for upstream fidelity. The preserve-upstream rules under Porting Packages in `AGENTS.md` exist for upstreams we keep merging from — Laravel first-party and Laravel-ecosystem packages — and Hyperf is neither: it's a historical reference. Adapt ported code fully to Hypervel structure, style, and naming, including cleaning up variable and method names, following this guide.

Expand Down
873 changes: 873 additions & 0 deletions docs/plans/2026-08-08-1300-driver-neutral-database-architecture.md

Large diffs are not rendered by default.

28 changes: 15 additions & 13 deletions docs/upstream-sync/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ For the mechanics of porting code (namespace changes, container conversion, serv

## Files in this directory

- **`sync.yaml`** — state file: last reviewed tag and last ported tag per package. Updated during every session. YAML (not markdown) because raw-in-IDE readability matters more than GitHub rendering for this one.
- **`<package>.md`** — per-package divergence notes. Created **lazily** when a real divergence is discovered. Never pre-stub empty files. Filename convention: gh repo slug with `/` replaced by `-` (e.g., `laravel-framework.md`, `orchestral-testbench.md`, `spatie-laravel-permission.md`).
- **`sync.yaml`** — state file: newest release fully reviewed and date of the most recent sync for each package. Updated during every session. Its `notes` field is only for sync-specific operational facts, such as a Composer package name that differs from the GitHub repository or the `laravel/framework` direct-to-branch scan.

Deliberate, lasting differences from Laravel belong in the affected package README under `Differences From Laravel`. Do not create per-package divergence files in this directory.

## Non-negotiable rules

Expand All @@ -25,15 +26,15 @@ For the mechanics of porting code (namespace changes, container conversion, serv

### Step 1 — Read state

Read `sync.yaml` top to bottom. For each package entry, note: the repo slug (top-level key), `release`, `sync_date`, and whether the `notes` field references a `<package>.md` divergence doc.
Read `sync.yaml` top to bottom. For each package entry, note the repo slug, `release`, `sync_date`, and any operational instructions in `notes`.

### Step 2 — Process each package

Work through the entries top to bottom. For each package:

**2a. Read divergence notes**
**2a. Read package guidance**

If the package's `notes` field references a `<package>.md` divergence doc, read it in full before proceeding. Skip this step if there is no divergence doc.
Before classifying or porting an upstream change, read the README for every Hypervel package it affects. Its `Differences From Laravel` section is the canonical record of deliberate, lasting public differences. Also check the relevant source and tests for comments recording intentionally omitted Laravel APIs or features.

**2b. Find new releases**

Expand Down Expand Up @@ -66,7 +67,7 @@ gh pr view <number> --repo <repo-slug>
Propose a classification and reasoning:

- **port** — take this change into Hypervel
- **skip** — intentionally not taken (state why: Laravel-Cloud-specific, PHP-FPM lifecycle, already diverged per `<package>.md`, already implemented differently in Hypervel, deprecated upstream path, etc.)
- **skip** — intentionally not taken (state why: Laravel-Cloud-specific, PHP-FPM lifecycle, conflicts with a deliberate difference recorded in the affected package README, already implemented differently in Hypervel, deprecated upstream path, etc.)
- **defer** — valid but blocked (state what is blocking it and what would unblock)

Wait for user approval on every classification. Never silently skip.
Expand All @@ -93,7 +94,7 @@ This check is **only required for `laravel/framework`**. Other packages release

**2f. Close out the release**

When every PR (and any direct commits) in the release has been decided and committed/recorded, bump `release` in `sync.yaml` to this release's tag. This happens regardless of whether anything was deferred deferred items are tracked in the session PR body (and in `<package>.md` if the blocker is persistent), not by holding the tag back.
When every PR (and any direct commits) in the release has been decided and committed/recorded, bump `release` in `sync.yaml` to this release's tag. This happens regardless of whether anything was deferred. Track deferred items in the session PR body and, when they remain worthwhile future work, in `docs/todo.md`; do not hold the tag back.

Then move to the next release for the same package.

Expand Down Expand Up @@ -148,15 +149,16 @@ If a session is interrupted mid-package:

Never leave `sync.yaml` in a state that misrepresents what was actually done.

## Per-package divergence notes (`<package>.md`)
## Recording differences and deferred work

Create a divergence note **only** when a real, concrete divergence is discovered that will affect future sync decisions. Contents:
Do not create package-specific divergence documents in this directory.

- **What Hypervel does differently** — the actual divergence
- **Why** — the concrete reason (Swoole semantics, architectural decision, deprecated upstream, etc.)
- **Sync implications** — what kinds of upstream PRs to skip or adapt going forward
- Record deliberate, lasting public differences from Laravel in the affected package README under `Differences From Laravel`.
- Record intentionally omitted Laravel APIs or features in the package README, source, and matching test location as required by `AGENTS.md`.
- Record deferred work in the session PR body and, when it remains worthwhile future work, in `docs/todo.md`.
- Use `sync.yaml` notes only for operational facts needed to process the upstream package.

Never speculate. Never pre-stub. If you find yourself writing a hypothetical, stop.
Do not duplicate package guidance in the sync workflow.

## Prerequisites

Expand Down
13 changes: 7 additions & 6 deletions docs/upstream-sync/sync.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@
# repo_url — GitHub URL, for humans
# release — newest release tag walked in a sync session. Bumped even if
# some PRs were deferred; deferred items are tracked in the
# session PR body and, if persistent, in <package>.md
# session PR body and, when they remain worthwhile future work,
# in docs/todo.md
# sync_date — date (YYYY-MM-DD) of the most recent session that touched
# this entry (bumped even if no releases were new)
# notes — optional, omit when empty. Permanent per-repo context that
# informs every sync of this entry (e.g. composer/repo name
# mismatch, per-repo workflow tweaks). Per-session notes
# belong in the session PR body, not here. Reference
# <package>.md here when a divergence doc exists.
# notes — optional, omit when empty. Sync-specific operational facts
# needed to process this repo (e.g. Composer/repo name mismatch
# or per-repo workflow tweaks). Per-session notes belong in the
# session PR body; package differences belong in the affected
# package README.
#
# `release` and `sync_date` are null on entries that haven't been synced yet.

Expand Down
50 changes: 31 additions & 19 deletions src/context/src/CoroutineContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ public static function copyFrom(int $fromCoroutineId, array $keys = []): void
/**
* Capture context values as an array.
*
* Replicable values are copied in the calling coroutine at capture time.
* Replicable values are copied and non-copyable values are omitted in the
* calling coroutine at capture time.
*
* @return array<TKey, TValue>
*/
Expand All @@ -133,13 +134,7 @@ public static function captureFrom(array $keys = [], ?int $fromCoroutineId = nul
? array_intersect_key($from->getArrayCopy(), array_flip($keys))
: $from->getArrayCopy();

foreach ($map as $key => $value) {
if ($value instanceof ReplicableContext) {
$map[$key] = $value->replicate();
}
}

return $map;
return self::prepareForCopy($map);
}

/**
Expand Down Expand Up @@ -225,10 +220,9 @@ public static function copyFromNonCoroutine(array $keys = [], ?int $coroutineId
$map = static::$nonCoroutineContext;
}

$map = self::prepareForCopy($map);

foreach ($map as $key => $value) {
if ($value instanceof ReplicableContext) {
$value = $value->replicate();
}
$context[$key] = $value;
}
}
Expand All @@ -245,17 +239,35 @@ public static function copyToNonCoroutine(array $keys = [], ?int $coroutineId =
return;
}

if ($keys) {
foreach ($keys as $key) {
if (isset($context[$key])) {
static::$nonCoroutineContext[$key] = $context[$key];
}
$map = $keys
? array_intersect_key($context->getArrayCopy(), array_flip($keys))
: $context->getArrayCopy();

$map = self::prepareForCopy($map);

foreach ($map as $key => $value) {
static::$nonCoroutineContext[(string) $key] = $value;
}
}

/**
* Prepare context values for copying.
*/
private static function prepareForCopy(array $values): array
{
foreach ($values as $key => $value) {
if ($value instanceof NonCopyableContext) {
unset($values[$key]);

continue;
}
} else {
foreach ($context as $key => $value) {
static::$nonCoroutineContext[$key] = $value;

if ($value instanceof ReplicableContext) {
$values[$key] = $value->replicate();
}
}

return $values;
}

/**
Expand Down
9 changes: 9 additions & 0 deletions src/context/src/NonCopyableContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Hypervel\Context;

interface NonCopyableContext
{
}
5 changes: 3 additions & 2 deletions src/coroutine/src/Parallel.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ class Parallel
* @param int $concurrent Maximum concurrent coroutines (0 = unlimited)
* @param array<string>|bool $copyContext When set, parent coroutine context is copied to each child.
* false = fresh context (default), true or empty array = copy all keys, non-empty array = copy listed keys only.
* Object values from the parent context are shared by reference; values implementing
* Hypervel\Context\ReplicableContext are deep-copied via replicate().
* Objects stored directly in context are shared by reference by default. Values implementing
* Hypervel\Context\ReplicableContext are copied via replicate(), while values implementing
* Hypervel\Context\NonCopyableContext are omitted.
*/
public function __construct(
int $concurrent = 0,
Expand Down
4 changes: 3 additions & 1 deletion src/coroutine/src/Waiter.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ public function __construct(float $timeout = self::DEFAULT_POP_TIMEOUT_SECONDS)
* @param null|float $timeout Timeout in seconds (null uses default)
* @param array<string>|bool $copyContext When set, parent coroutine context is copied to the child.
* false = fresh context (default), true or empty array = copy all keys, non-empty array = copy listed keys only.
* Object values are shared by reference unless they implement Hypervel\Context\ReplicableContext.
* Objects stored directly in context are shared by reference by default. Values implementing
* Hypervel\Context\ReplicableContext are copied via replicate(), while values implementing
* Hypervel\Context\NonCopyableContext are omitted.
* @param bool $waitForChildTermination Wait without a limit when a cancelled child exceeds the cleanup allowance
* @return TReturn
* @throws WaitTimeoutException When the wait times out
Expand Down
16 changes: 12 additions & 4 deletions src/coroutine/src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
* @param int $concurrent if $concurrent is equal to 0, that means unlimited
* @param array<string>|bool $copyContext When set, parent coroutine context is copied to each child.
* false = fresh context (default), true or empty array = copy all keys, non-empty array = copy listed keys only.
* Object values are shared by reference unless they implement Hypervel\Context\ReplicableContext.
* Objects stored directly in context are shared by reference by default. Values implementing
* Hypervel\Context\ReplicableContext are copied via replicate(), while values implementing
* Hypervel\Context\NonCopyableContext are omitted.
*/
function parallel(array $callables, int $concurrent = 0, bool|array $copyContext = false): array
{
Expand All @@ -33,7 +35,9 @@ function parallel(array $callables, int $concurrent = 0, bool|array $copyContext
* @param Closure():TReturn $closure
* @param array<string>|bool $copyContext When set, parent coroutine context is copied to the child.
* false = fresh context (default), true or empty array = copy all keys, non-empty array = copy listed keys only.
* Object values are shared by reference unless they implement Hypervel\Context\ReplicableContext.
* Objects stored directly in context are shared by reference by default. Values implementing
* Hypervel\Context\ReplicableContext are copied via replicate(), while values implementing
* Hypervel\Context\NonCopyableContext are omitted.
* @param bool $waitForChildTermination Wait without a limit when a cancelled child exceeds the cleanup allowance
* @return TReturn
* @throws WaitTimeoutException When the wait times out
Expand All @@ -53,7 +57,9 @@ function wait(
/**
* @param array<string>|bool $copyContext When set, parent coroutine context is copied to the child.
* false = fresh context (default), true or empty array = copy all keys, non-empty array = copy listed keys only.
* Object values are shared by reference unless they implement Hypervel\Context\ReplicableContext.
* Objects stored directly in context are shared by reference by default. Values implementing
* Hypervel\Context\ReplicableContext are copied via replicate(), while values implementing
* Hypervel\Context\NonCopyableContext are omitted.
*/
function co(callable $callable, bool|array $copyContext = false): int
{
Expand All @@ -70,7 +76,9 @@ function co(callable $callable, bool|array $copyContext = false): int
/**
* @param array<string>|bool $copyContext When set, parent coroutine context is copied to the child.
* false = fresh context (default), true or empty array = copy all keys, non-empty array = copy listed keys only.
* Object values are shared by reference unless they implement Hypervel\Context\ReplicableContext.
* Objects stored directly in context are shared by reference by default. Values implementing
* Hypervel\Context\ReplicableContext are copied via replicate(), while values implementing
* Hypervel\Context\NonCopyableContext are omitted.
*/
function go(callable $callable, bool|array $copyContext = false): int
{
Expand Down
Loading