From d00e174e753d9e52bc947c1911198b774188ee6e Mon Sep 17 00:00:00 2001 From: toby7002 Date: Fri, 22 Sep 2023 20:56:57 +0700 Subject: [PATCH] Event loop! --- composer.lock | 3020 +++++++++++++++++ src/thebigcrafter/Hydrogen/EventLoop.php | 420 +++ .../Hydrogen/eventLoop/CallbackType.php | 22 + .../Hydrogen/eventLoop/Driver.php | 321 ++ .../Hydrogen/eventLoop/Driver/EvDriver.php | 232 ++ .../Hydrogen/eventLoop/Driver/EventDriver.php | 257 ++ .../eventLoop/Driver/StreamSelectDriver.php | 358 ++ .../eventLoop/Driver/TracingDriver.php | 286 ++ .../Hydrogen/eventLoop/Driver/UvDriver.php | 289 ++ .../Hydrogen/eventLoop/DriverFactory.php | 87 + .../Hydrogen/eventLoop/FiberLocal.php | 89 + .../eventLoop/Internal/AbstractDriver.php | 648 ++++ .../eventLoop/Internal/ClosureHelper.php | 37 + .../eventLoop/Internal/DeferCallback.php | 17 + .../eventLoop/Internal/DriverCallback.php | 40 + .../eventLoop/Internal/DriverSuspension.php | 179 + .../eventLoop/Internal/SignalCallback.php | 24 + .../eventLoop/Internal/StreamCallback.php | 27 + .../Internal/StreamReadableCallback.php | 17 + .../Internal/StreamWritableCallback.php | 17 + .../eventLoop/Internal/TimerCallback.php | 26 + .../eventLoop/Internal/TimerQueue.php | 166 + .../eventLoop/InvalidCallbackError.php | 82 + .../Hydrogen/eventLoop/Suspension.php | 50 + .../Hydrogen/eventLoop/UncaughtThrowable.php | 44 + .../eventLoop/UnsupportedFeatureException.php | 21 + 26 files changed, 6776 insertions(+) create mode 100644 composer.lock create mode 100644 src/thebigcrafter/Hydrogen/EventLoop.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/CallbackType.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/Driver.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/Driver/EvDriver.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/Driver/EventDriver.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/Driver/StreamSelectDriver.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/Driver/TracingDriver.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/Driver/UvDriver.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/DriverFactory.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/FiberLocal.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/Internal/AbstractDriver.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/Internal/ClosureHelper.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/Internal/DeferCallback.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/Internal/DriverCallback.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/Internal/DriverSuspension.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/Internal/SignalCallback.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/Internal/StreamCallback.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/Internal/StreamReadableCallback.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/Internal/StreamWritableCallback.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/Internal/TimerCallback.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/Internal/TimerQueue.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/InvalidCallbackError.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/Suspension.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/UncaughtThrowable.php create mode 100644 src/thebigcrafter/Hydrogen/eventLoop/UnsupportedFeatureException.php diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..e6a3cfc --- /dev/null +++ b/composer.lock @@ -0,0 +1,3020 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "03b96cbceedb504ba98370e14aa99338", + "packages": [ + { + "name": "adhocore/json-comment", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/adhocore/php-json-comment.git", + "reference": "651023f9fe52e9efa2198cbaf6e481d1968e2377" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/adhocore/php-json-comment/zipball/651023f9fe52e9efa2198cbaf6e481d1968e2377", + "reference": "651023f9fe52e9efa2198cbaf6e481d1968e2377", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.5 || ^7.5 || ^8.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ahc\\Json\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jitendra Adhikari", + "email": "jiten.adhikary@gmail.com" + } + ], + "description": "Lightweight JSON comment stripper library for PHP", + "keywords": [ + "comment", + "json", + "strip-comment" + ], + "support": { + "issues": "https://github.com/adhocore/php-json-comment/issues", + "source": "https://github.com/adhocore/php-json-comment/tree/1.2.1" + }, + "funding": [ + { + "url": "https://paypal.me/ji10", + "type": "custom" + }, + { + "url": "https://github.com/adhocore", + "type": "github" + } + ], + "time": "2022-10-02T11:22:07+00:00" + }, + { + "name": "brick/math", + "version": "0.11.0", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "0ad82ce168c82ba30d1c01ec86116ab52f589478" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/0ad82ce168c82ba30d1c01ec86116ab52f589478", + "reference": "0ad82ce168c82ba30d1c01ec86116ab52f589478", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^9.0", + "vimeo/psalm": "5.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "brick", + "math" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.11.0" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2023-01-15T23:15:59+00:00" + }, + { + "name": "dktapps/pmforms", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/dktapps-pm-pl/pmforms.git", + "reference": "ea46df4e4c73675010fa860be5db6fbda60a85d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dktapps-pm-pl/pmforms/zipball/ea46df4e4c73675010fa860be5db6fbda60a85d4", + "reference": "ea46df4e4c73675010fa860be5db6fbda60a85d4", + "shasum": "" + }, + "require": { + "pocketmine/pocketmine-mp": "^4.0.0 || ^5.0.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.2.0", + "phpstan/phpstan-strict-rules": "^1.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "virion": { + "spec": "3.0", + "namespace-root": "dktapps\\pmforms" + } + }, + "autoload": { + "psr-4": { + "": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "Form API library for PocketMine-MP plugins", + "support": { + "issues": "https://github.com/dktapps-pm-pl/pmforms/issues", + "source": "https://github.com/dktapps-pm-pl/pmforms/tree/master" + }, + "time": "2023-08-15T19:02:00+00:00" + }, + { + "name": "pocketmine/bedrock-block-upgrade-schema", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/pmmp/BedrockBlockUpgradeSchema.git", + "reference": "874e1c0c9b7b620744d08b59c78354fe9f028dfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/BedrockBlockUpgradeSchema/zipball/874e1c0c9b7b620744d08b59c78354fe9f028dfa", + "reference": "874e1c0c9b7b620744d08b59c78354fe9f028dfa", + "shasum": "" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "CC0-1.0" + ], + "description": "Schemas describing how to upgrade saved block data in older Minecraft: Bedrock Edition world saves", + "support": { + "issues": "https://github.com/pmmp/BedrockBlockUpgradeSchema/issues", + "source": "https://github.com/pmmp/BedrockBlockUpgradeSchema/tree/3.2.0" + }, + "time": "2023-09-20T17:03:43+00:00" + }, + { + "name": "pocketmine/bedrock-data", + "version": "2.5.0+bedrock-1.20.30", + "source": { + "type": "git", + "url": "https://github.com/pmmp/BedrockData.git", + "reference": "e920209393a8bf6cb15fb40c3f3149aaf8e1a2b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/BedrockData/zipball/e920209393a8bf6cb15fb40c3f3149aaf8e1a2b9", + "reference": "e920209393a8bf6cb15fb40c3f3149aaf8e1a2b9", + "shasum": "" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "CC0-1.0" + ], + "description": "Blobs of data generated from Minecraft: Bedrock Edition, used by PocketMine-MP", + "support": { + "issues": "https://github.com/pmmp/BedrockData/issues", + "source": "https://github.com/pmmp/BedrockData/tree/bedrock-1.20.30" + }, + "time": "2023-09-20T16:34:21+00:00" + }, + { + "name": "pocketmine/bedrock-item-upgrade-schema", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/pmmp/BedrockItemUpgradeSchema.git", + "reference": "3edc9ebbad9a4f2d9c8f53b3a5ba44d4a792ad93" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/BedrockItemUpgradeSchema/zipball/3edc9ebbad9a4f2d9c8f53b3a5ba44d4a792ad93", + "reference": "3edc9ebbad9a4f2d9c8f53b3a5ba44d4a792ad93", + "shasum": "" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "CC0-1.0" + ], + "description": "JSON schemas for upgrading items found in older Minecraft: Bedrock world saves", + "support": { + "issues": "https://github.com/pmmp/BedrockItemUpgradeSchema/issues", + "source": "https://github.com/pmmp/BedrockItemUpgradeSchema/tree/1.5.0" + }, + "time": "2023-09-01T19:58:57+00:00" + }, + { + "name": "pocketmine/bedrock-protocol", + "version": "24.0.0+bedrock-1.20.30", + "source": { + "type": "git", + "url": "https://github.com/pmmp/BedrockProtocol.git", + "reference": "38a516274aa6641b0da38011af35a5587fc87895" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/38a516274aa6641b0da38011af35a5587fc87895", + "reference": "38a516274aa6641b0da38011af35a5587fc87895", + "shasum": "" + }, + "require": { + "ext-json": "*", + "netresearch/jsonmapper": "^4.0", + "php": "^8.1", + "pocketmine/binaryutils": "^0.2.0", + "pocketmine/color": "^0.2.0 || ^0.3.0", + "pocketmine/math": "^0.3.0 || ^0.4.0 || ^1.0.0", + "pocketmine/nbt": "^1.0.0", + "ramsey/uuid": "^4.1" + }, + "require-dev": { + "phpstan/phpstan": "1.10.33", + "phpstan/phpstan-phpunit": "^1.0.0", + "phpstan/phpstan-strict-rules": "^1.0.0", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "pocketmine\\network\\mcpe\\protocol\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "An implementation of the Minecraft: Bedrock Edition protocol in PHP", + "support": { + "issues": "https://github.com/pmmp/BedrockProtocol/issues", + "source": "https://github.com/pmmp/BedrockProtocol/tree/24.0.0+bedrock-1.20.30" + }, + "time": "2023-09-20T16:57:53+00:00" + }, + { + "name": "pocketmine/binaryutils", + "version": "0.2.4", + "source": { + "type": "git", + "url": "https://github.com/pmmp/BinaryUtils.git", + "reference": "5ac7eea91afbad8dc498f5ce34ce6297d5e6ea9a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/BinaryUtils/zipball/5ac7eea91afbad8dc498f5ce34ce6297d5e6ea9a", + "reference": "5ac7eea91afbad8dc498f5ce34ce6297d5e6ea9a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "php-64bit": "*" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "1.3.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0.0", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "pocketmine\\utils\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "Classes and methods for conveniently handling binary data", + "support": { + "issues": "https://github.com/pmmp/BinaryUtils/issues", + "source": "https://github.com/pmmp/BinaryUtils/tree/0.2.4" + }, + "time": "2022-01-12T18:06:33+00:00" + }, + { + "name": "pocketmine/callback-validator", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/pmmp/CallbackValidator.git", + "reference": "64787469766bcaa7e5885242e85c23c25e8c55a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/CallbackValidator/zipball/64787469766bcaa7e5885242e85c23c25e8c55a2", + "reference": "64787469766bcaa7e5885242e85c23c25e8c55a2", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 || ^8.0" + }, + "replace": { + "daverandom/callback-validator": "*" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "0.12.59", + "phpstan/phpstan-strict-rules": "^0.12.4", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "DaveRandom\\CallbackValidator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Wright", + "email": "cw@daverandom.com" + } + ], + "description": "Fork of daverandom/callback-validator - Tools for validating callback signatures", + "support": { + "issues": "https://github.com/pmmp/CallbackValidator/issues", + "source": "https://github.com/pmmp/CallbackValidator/tree/1.0.3" + }, + "time": "2020-12-11T01:45:37+00:00" + }, + { + "name": "pocketmine/color", + "version": "0.3.1", + "source": { + "type": "git", + "url": "https://github.com/pmmp/Color.git", + "reference": "a0421f1e9e0b0c619300fb92d593283378f6a5e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/Color/zipball/a0421f1e9e0b0c619300fb92d593283378f6a5e1", + "reference": "a0421f1e9e0b0c619300fb92d593283378f6a5e1", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.3", + "phpstan/phpstan-strict-rules": "^1.2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "pocketmine\\color\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "Color handling library used by PocketMine-MP and related projects", + "support": { + "issues": "https://github.com/pmmp/Color/issues", + "source": "https://github.com/pmmp/Color/tree/0.3.1" + }, + "time": "2023-04-10T11:38:05+00:00" + }, + { + "name": "pocketmine/errorhandler", + "version": "0.6.0", + "source": { + "type": "git", + "url": "https://github.com/pmmp/ErrorHandler.git", + "reference": "dae214a04348b911e8219ebf125ff1c5589cc878" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/ErrorHandler/zipball/dae214a04348b911e8219ebf125ff1c5589cc878", + "reference": "dae214a04348b911e8219ebf125ff1c5589cc878", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "phpstan/phpstan": "0.12.99", + "phpstan/phpstan-strict-rules": "^0.12.2", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "pocketmine\\errorhandler\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "Utilities to handle nasty PHP E_* errors in a usable way", + "support": { + "issues": "https://github.com/pmmp/ErrorHandler/issues", + "source": "https://github.com/pmmp/ErrorHandler/tree/0.6.0" + }, + "time": "2022-01-08T21:05:46+00:00" + }, + { + "name": "pocketmine/locale-data", + "version": "2.19.6", + "source": { + "type": "git", + "url": "https://github.com/pmmp/Language.git", + "reference": "93e473e20e7f4515ecf45c5ef0f9155b9247a86e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/Language/zipball/93e473e20e7f4515ecf45c5ef0f9155b9247a86e", + "reference": "93e473e20e7f4515ecf45c5ef0f9155b9247a86e", + "shasum": "" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "description": "Language resources used by PocketMine-MP", + "support": { + "issues": "https://github.com/pmmp/Language/issues", + "source": "https://github.com/pmmp/Language/tree/2.19.6" + }, + "time": "2023-08-08T16:53:23+00:00" + }, + { + "name": "pocketmine/log", + "version": "0.4.0", + "source": { + "type": "git", + "url": "https://github.com/pmmp/Log.git", + "reference": "e6c912c0f9055c81d23108ec2d179b96f404c043" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/Log/zipball/e6c912c0f9055c81d23108ec2d179b96f404c043", + "reference": "e6c912c0f9055c81d23108ec2d179b96f404c043", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "pocketmine/spl": "<0.4" + }, + "require-dev": { + "phpstan/phpstan": "0.12.88", + "phpstan/phpstan-strict-rules": "^0.12.2" + }, + "type": "library", + "autoload": { + "classmap": [ + "./src" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "Logging components used by PocketMine-MP and related projects", + "support": { + "issues": "https://github.com/pmmp/Log/issues", + "source": "https://github.com/pmmp/Log/tree/0.4.0" + }, + "time": "2021-06-18T19:08:09+00:00" + }, + { + "name": "pocketmine/math", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/pmmp/Math.git", + "reference": "dc132d93595b32e9f210d78b3c8d43c662a5edbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/Math/zipball/dc132d93595b32e9f210d78b3c8d43c662a5edbf", + "reference": "dc132d93595b32e9f210d78b3c8d43c662a5edbf", + "shasum": "" + }, + "require": { + "php": "^8.0", + "php-64bit": "*" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "~1.10.3", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^8.5 || ^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "pocketmine\\math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "PHP library containing math related code used in PocketMine-MP", + "support": { + "issues": "https://github.com/pmmp/Math/issues", + "source": "https://github.com/pmmp/Math/tree/1.0.0" + }, + "time": "2023-08-03T12:56:33+00:00" + }, + { + "name": "pocketmine/nbt", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/pmmp/NBT.git", + "reference": "20540271cb59e04672cb163dca73366f207974f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/NBT/zipball/20540271cb59e04672cb163dca73366f207974f1", + "reference": "20540271cb59e04672cb163dca73366f207974f1", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "php-64bit": "*", + "pocketmine/binaryutils": "^0.2.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "1.10.25", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "pocketmine\\nbt\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "PHP library for working with Named Binary Tags", + "support": { + "issues": "https://github.com/pmmp/NBT/issues", + "source": "https://github.com/pmmp/NBT/tree/1.0.0" + }, + "time": "2023-07-14T13:01:49+00:00" + }, + { + "name": "pocketmine/netresearch-jsonmapper", + "version": "v4.2.1000", + "source": { + "type": "git", + "url": "https://github.com/pmmp/netresearch-jsonmapper.git", + "reference": "078764e869e9b732f97206ec9363480a77c35532" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/netresearch-jsonmapper/zipball/078764e869e9b732f97206ec9363480a77c35532", + "reference": "078764e869e9b732f97206ec9363480a77c35532", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=7.1" + }, + "replace": { + "netresearch/jsonmapper": "~4.2.0" + }, + "require-dev": { + "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0", + "squizlabs/php_codesniffer": "~3.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "JsonMapper": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@cweiske.de", + "homepage": "http://github.com/cweiske/jsonmapper/", + "role": "Developer" + } + ], + "description": "Fork of netresearch/jsonmapper with security fixes needed by pocketmine/pocketmine-mp", + "support": { + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues", + "source": "https://github.com/pmmp/netresearch-jsonmapper/tree/v4.2.1000" + }, + "time": "2023-07-14T10:44:14+00:00" + }, + { + "name": "pocketmine/pocketmine-mp", + "version": "5.6.0", + "source": { + "type": "git", + "url": "https://github.com/pmmp/PocketMine-MP.git", + "reference": "338bb3fe6d70f093fc8dc01d1d3c3b4b3656fcf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/PocketMine-MP/zipball/338bb3fe6d70f093fc8dc01d1d3c3b4b3656fcf8", + "reference": "338bb3fe6d70f093fc8dc01d1d3c3b4b3656fcf8", + "shasum": "" + }, + "require": { + "adhocore/json-comment": "~1.2.0", + "composer-runtime-api": "^2.0", + "ext-chunkutils2": "^0.3.1", + "ext-crypto": "^0.3.1", + "ext-ctype": "*", + "ext-curl": "*", + "ext-date": "*", + "ext-gmp": "*", + "ext-hash": "*", + "ext-igbinary": "^3.0.1", + "ext-json": "*", + "ext-leveldb": "^0.2.1 || ^0.3.0", + "ext-mbstring": "*", + "ext-morton": "^0.1.0", + "ext-openssl": "*", + "ext-pcre": "*", + "ext-phar": "*", + "ext-pmmpthread": "^6.0.7", + "ext-reflection": "*", + "ext-simplexml": "*", + "ext-sockets": "*", + "ext-spl": "*", + "ext-yaml": ">=2.0.0", + "ext-zip": "*", + "ext-zlib": ">=1.2.11", + "php": "^8.1", + "php-64bit": "*", + "pocketmine/bedrock-block-upgrade-schema": "~3.2.0+bedrock-1.20.30", + "pocketmine/bedrock-data": "~2.5.0+bedrock-1.20.30", + "pocketmine/bedrock-item-upgrade-schema": "~1.5.0+bedrock-1.20.30", + "pocketmine/bedrock-protocol": "~24.0.0+bedrock-1.20.30", + "pocketmine/binaryutils": "^0.2.1", + "pocketmine/callback-validator": "^1.0.2", + "pocketmine/color": "^0.3.0", + "pocketmine/errorhandler": "^0.6.0", + "pocketmine/locale-data": "~2.19.0", + "pocketmine/log": "^0.4.0", + "pocketmine/math": "~1.0.0", + "pocketmine/nbt": "~1.0.0", + "pocketmine/netresearch-jsonmapper": "~v4.2.1000", + "pocketmine/raklib": "^0.15.0", + "pocketmine/raklib-ipc": "^0.2.0", + "pocketmine/snooze": "^0.5.0", + "ramsey/uuid": "~4.7.0", + "symfony/filesystem": "~6.3.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.16", + "phpstan/phpstan-phpunit": "^1.1.0", + "phpstan/phpstan-strict-rules": "^1.2.0", + "phpunit/phpunit": "^10.1" + }, + "type": "project", + "autoload": { + "files": [ + "src/CoreConstants.php" + ], + "psr-4": { + "pocketmine\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "A server software for Minecraft: Bedrock Edition written in PHP", + "homepage": "https://pmmp.io", + "support": { + "issues": "https://github.com/pmmp/PocketMine-MP/issues", + "source": "https://github.com/pmmp/PocketMine-MP/tree/5.6.0" + }, + "funding": [ + { + "url": "https://github.com/pmmp/PocketMine-MP#donate", + "type": "custom" + }, + { + "url": "https://www.patreon.com/pocketminemp", + "type": "patreon" + } + ], + "time": "2023-09-20T18:00:51+00:00" + }, + { + "name": "pocketmine/raklib", + "version": "0.15.1", + "source": { + "type": "git", + "url": "https://github.com/pmmp/RakLib.git", + "reference": "79b7b4d1d7516dc6e322514453645ad9452b20ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/RakLib/zipball/79b7b4d1d7516dc6e322514453645ad9452b20ca", + "reference": "79b7b4d1d7516dc6e322514453645ad9452b20ca", + "shasum": "" + }, + "require": { + "ext-sockets": "*", + "php": "^8.0", + "php-64bit": "*", + "php-ipv6": "*", + "pocketmine/binaryutils": "^0.2.0", + "pocketmine/log": "^0.3.0 || ^0.4.0" + }, + "require-dev": { + "phpstan/phpstan": "1.9.17", + "phpstan/phpstan-strict-rules": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "raklib\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0" + ], + "description": "A RakNet server implementation written in PHP", + "support": { + "issues": "https://github.com/pmmp/RakLib/issues", + "source": "https://github.com/pmmp/RakLib/tree/0.15.1" + }, + "time": "2023-03-07T15:10:34+00:00" + }, + { + "name": "pocketmine/raklib-ipc", + "version": "0.2.0", + "source": { + "type": "git", + "url": "https://github.com/pmmp/RakLibIpc.git", + "reference": "26ed56fa9db06e4ca6e8920c0ede2e01e219bb9c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/RakLibIpc/zipball/26ed56fa9db06e4ca6e8920c0ede2e01e219bb9c", + "reference": "26ed56fa9db06e4ca6e8920c0ede2e01e219bb9c", + "shasum": "" + }, + "require": { + "php": "^8.0", + "php-64bit": "*", + "pocketmine/binaryutils": "^0.2.0", + "pocketmine/raklib": "^0.15.0" + }, + "require-dev": { + "phpstan/phpstan": "1.9.17", + "phpstan/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "raklib\\server\\ipc\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0" + ], + "description": "Channel-based protocols for inter-thread/inter-process communication with RakLib", + "support": { + "issues": "https://github.com/pmmp/RakLibIpc/issues", + "source": "https://github.com/pmmp/RakLibIpc/tree/0.2.0" + }, + "time": "2023-02-13T13:40:40+00:00" + }, + { + "name": "pocketmine/snooze", + "version": "0.5.0", + "source": { + "type": "git", + "url": "https://github.com/pmmp/Snooze.git", + "reference": "a86d9ee60ce44755d166d3c7ba4b8b8be8360915" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/Snooze/zipball/a86d9ee60ce44755d166d3c7ba4b8b8be8360915", + "reference": "a86d9ee60ce44755d166d3c7ba4b8b8be8360915", + "shasum": "" + }, + "require": { + "ext-pmmpthread": "^6.0", + "php-64bit": "^8.1" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "1.10.3", + "phpstan/phpstan-strict-rules": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "pocketmine\\snooze\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "Thread notification management library for code using the pthreads extension", + "support": { + "issues": "https://github.com/pmmp/Snooze/issues", + "source": "https://github.com/pmmp/Snooze/tree/0.5.0" + }, + "time": "2023-05-22T23:43:01+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.28.3", + "fakerphp/faker": "^1.21", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^1.0", + "mockery/mockery": "^1.5", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpcsstandards/phpcsutils": "^1.0.0-rc1", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5", + "psalm/plugin-mockery": "^1.1", + "psalm/plugin-phpunit": "^0.18.4", + "ramsey/coding-standard": "^2.0.3", + "ramsey/conventional-commits": "^1.3", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", + "type": "tidelift" + } + ], + "time": "2022-12-31T21:50:55+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.7.4", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "60a4c63ab724854332900504274f6150ff26d286" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/60a4c63ab724854332900504274f6150ff26d286", + "reference": "60a4c63ab724854332900504274f6150ff26d286", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11", + "ext-json": "*", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.10", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "doctrine/annotations": "^1.8", + "ergebnis/composer-normalize": "^2.15", + "mockery/mockery": "^1.3", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.2", + "php-mock/php-mock-mockery": "^1.3", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpbench/phpbench": "^1.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^8.5 || ^9", + "ramsey/composer-repl": "^1.4", + "slevomat/coding-standard": "^8.4", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.9" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.7.4" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", + "type": "tidelift" + } + ], + "time": "2023-04-15T23:01:58+00:00" + }, + { + "name": "sof3/await-generator", + "version": "3.6.1", + "source": { + "type": "git", + "url": "https://github.com/SOF3/await-generator.git", + "reference": "90f4dda776eaf9bcb4693599c61c42e4c584a73f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SOF3/await-generator/zipball/90f4dda776eaf9bcb4693599c61c42e4c584a73f", + "reference": "90f4dda776eaf9bcb4693599c61c42e4c584a73f", + "shasum": "" + }, + "require": { + "ext-bcmath": "*", + "php": "^8.0" + }, + "require-dev": { + "composer/package-versions-deprecated": "1.11.99.1", + "infection/infection": "^0.18.2 || ^0.20.2 || ^0.26.0", + "phpstan/phpstan": "^0.12.84", + "phpunit/phpunit": "^9" + }, + "type": "library", + "extra": { + "virion": { + "spec": "3.0", + "namespace-root": "SOFe\\AwaitGenerator" + } + }, + "autoload": { + "psr-0": { + "SOFe\\AwaitGenerator\\": "await-generator/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "apache-2.0" + ], + "authors": [ + { + "name": "SOFe", + "email": "sofe2038@gmail.com" + } + ], + "description": "Use async/await in PHP using generators", + "support": { + "issues": "https://github.com/SOF3/await-generator/issues", + "source": "https://github.com/SOF3/await-generator/tree/3.6.1" + }, + "time": "2023-07-27T09:45:08+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v6.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "edd36776956f2a6fcf577edb5b05eb0e3bdc52ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/edd36776956f2a6fcf577edb5b05eb0e3bdc52ae", + "reference": "edd36776956f2a6fcf577edb5b05eb0e3bdc52ae", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v6.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-06-01T08:30:39+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "42292d99c55abe617799667f454222c54c60e229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-28T09:04:16+00:00" + } + ], + "packages-dev": [ + { + "name": "composer/pcre", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/4bff79ddd77851fe3cdd11616ed3f92841ba5bd2", + "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.1.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-11-17T09:50:14+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-08-31T09:50:34+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "ced299686f41dce890debac69273b47ffe98a40c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", + "reference": "ced299686f41dce890debac69273b47ffe98a40c", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-02-25T21:32:43+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v3.27.0", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "e73ccaae1208f017bb7860986eebb3da48bd25d6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/e73ccaae1208f017bb7860986eebb3da48bd25d6", + "reference": "e73ccaae1208f017bb7860986eebb3da48bd25d6", + "shasum": "" + }, + "require": { + "composer/semver": "^3.3", + "composer/xdebug-handler": "^3.0.3", + "ext-json": "*", + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0", + "sebastian/diff": "^4.0 || ^5.0", + "symfony/console": "^5.4 || ^6.0", + "symfony/event-dispatcher": "^5.4 || ^6.0", + "symfony/filesystem": "^5.4 || ^6.0", + "symfony/finder": "^5.4 || ^6.0", + "symfony/options-resolver": "^5.4 || ^6.0", + "symfony/polyfill-mbstring": "^1.27", + "symfony/polyfill-php80": "^1.27", + "symfony/polyfill-php81": "^1.27", + "symfony/process": "^5.4 || ^6.0", + "symfony/stopwatch": "^5.4 || ^6.0" + }, + "require-dev": { + "facile-it/paraunit": "^1.3 || ^2.0", + "justinrainbow/json-schema": "^5.2", + "keradus/cli-executor": "^2.0", + "mikey179/vfsstream": "^1.6.11", + "php-coveralls/php-coveralls": "^2.5.3", + "php-cs-fixer/accessible-object": "^1.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", + "phpspec/prophecy": "^1.16", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "phpunitgoodpractices/polyfill": "^1.6", + "phpunitgoodpractices/traits": "^1.9.2", + "symfony/phpunit-bridge": "^6.2.3", + "symfony/yaml": "^5.4 || ^6.0" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "keywords": [ + "Static code analysis", + "fixer", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.27.0" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2023-09-17T14:37:54+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "1.10.35", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "e730e5facb75ffe09dfb229795e8c01a459f26c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e730e5facb75ffe09dfb229795e8c01a459f26c3", + "reference": "e730e5facb75ffe09dfb229795e8c01a459f26c3", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], + "time": "2023-09-19T15:27:56+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/log", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.0" + }, + "time": "2021-07-14T16:46:02+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "912dc2fbe3e3c1e7873313cc801b100b6c68c87b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/912dc2fbe3e3c1e7873313cc801b100b6c68c87b", + "reference": "912dc2fbe3e3c1e7873313cc801b100b6c68c87b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-05-01T07:48:21+00:00" + }, + { + "name": "symfony/console", + "version": "v6.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "eca495f2ee845130855ddf1cf18460c38966c8b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/eca495f2ee845130855ddf1cf18460c38966c8b6", + "reference": "eca495f2ee845130855ddf1cf18460c38966c8b6", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/lock": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-08-16T10:10:12+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v6.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "adb01fe097a4ee930db9258a3cc906b5beb5cf2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/adb01fe097a4ee930db9258a3cc906b5beb5cf2e", + "reference": "adb01fe097a4ee930db9258a3cc906b5beb5cf2e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/error-handler": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v6.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-06T06:56:43+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/a76aed96a42d2b521153fb382d418e30d18b59df", + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "9915db259f67d21eefee768c1abcf1cc61b1fc9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/9915db259f67d21eefee768c1abcf1cc61b1fc9e", + "reference": "9915db259f67d21eefee768c1abcf1cc61b1fc9e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v6.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-31T08:31:44+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v6.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "a10f19f5198d589d5c33333cffe98dc9820332dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/a10f19f5198d589d5c33333cffe98dc9820332dd", + "reference": "a10f19f5198d589d5c33333cffe98dc9820332dd", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v6.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-12T14:21:09+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "875e90aeea2777b6f135677f618529449334a612" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", + "reference": "875e90aeea2777b6f135677f618529449334a612", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/7581cd600fa9fd681b797d00b02f068e2f13263b", + "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/process", + "version": "v6.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/0b5c29118f2e980d455d2e34a5659f4579847c54", + "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v6.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-08-07T10:39:22+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", + "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v6.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "fc47f1015ec80927ff64ba9094dfe8b9d48fe9f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/fc47f1015ec80927ff64ba9094dfe8b9d48fe9f2", + "reference": "fc47f1015ec80927ff64ba9094dfe8b9d48fe9f2", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v6.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-16T10:14:28+00:00" + }, + { + "name": "symfony/string", + "version": "v6.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "53d1a83225002635bca3482fcbf963001313fb68" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/53d1a83225002635bca3482fcbf963001313fb68", + "reference": "53d1a83225002635bca3482fcbf963001313fb68", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/error-handler": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/intl": "^6.2", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v6.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-05T08:41:27+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "dktapps/pmforms": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.1" + }, + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/src/thebigcrafter/Hydrogen/EventLoop.php b/src/thebigcrafter/Hydrogen/EventLoop.php new file mode 100644 index 0000000..2a7d880 --- /dev/null +++ b/src/thebigcrafter/Hydrogen/EventLoop.php @@ -0,0 +1,420 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +namespace thebigcrafter\Hydrogen; + +use thebigcrafter\Hydrogen\eventLoop\CallbackType; +use thebigcrafter\Hydrogen\eventLoop\Driver; +use thebigcrafter\Hydrogen\eventLoop\DriverFactory; +use thebigcrafter\Hydrogen\eventLoop\Internal\AbstractDriver; +use thebigcrafter\Hydrogen\eventLoop\Internal\DriverCallback; +use thebigcrafter\Hydrogen\eventLoop\Suspension; +use thebigcrafter\Hydrogen\eventLoop\UnsupportedFeatureException; +use function gc_collect_cycles; +use function hrtime; + +/** + * Accessor to allow global access to the event loop. + * + * @see Driver + */ +final class EventLoop +{ + private static Driver $driver; + + /** + * Sets the driver to be used as the event loop. + */ + public static function setDriver(Driver $driver) : void + { + /** @psalm-suppress RedundantPropertyInitializationCheck, RedundantCondition */ + if (isset(self::$driver) && self::$driver->isRunning()) { + throw new \Error("Can't swap the event loop driver while the driver is running"); + } + + try { + /** @psalm-suppress InternalClass */ + self::$driver = new class extends AbstractDriver { + protected function activate(array $callbacks) : void + { + throw new \Error("Can't activate callback during garbage collection."); + } + + protected function dispatch(bool $blocking) : void + { + throw new \Error("Can't dispatch during garbage collection."); + } + + protected function deactivate(DriverCallback $callback) : void + { + // do nothing + } + + public function getHandle() : mixed + { + return null; + } + + protected function now() : float + { + return (float) hrtime(true) / 1_000_000_000; + } + }; + + gc_collect_cycles(); + } finally { + self::$driver = $driver; + } + } + + /** + * Queue a microtask. + * + * The queued callback MUST be executed immediately once the event loop gains control. Order of queueing MUST be + * preserved when executing the callbacks. Recursive scheduling can thus result in infinite loops, use with care. + * + * Does NOT create an event callback, thus CAN NOT be marked as disabled or unreferenced. + * Use {@see EventLoop::defer()} if you need these features. + * + * @param \Closure(...):void $closure The callback to queue. + * @param mixed ...$args The callback arguments. + */ + public static function queue(\Closure $closure, mixed ...$args) : void + { + self::getDriver()->queue($closure, ...$args); + } + + /** + * Defer the execution of a callback. + * + * The deferred callback MUST be executed before any other type of callback in a tick. Order of enabling MUST be + * preserved when executing the callbacks. + * + * The created callback MUST immediately be marked as enabled, but only be activated (i.e. callback can be called) + * right before the next tick. Deferred callbacks MUST NOT be called in the tick they were enabled. + * + * @param \Closure(string):void $closure The callback to defer. The `$callbackId` will be + * invalidated before the callback invocation. + * + * @return string A unique identifier that can be used to cancel, enable or disable the callback. + */ + public static function defer(\Closure $closure) : string + { + return self::getDriver()->defer($closure); + } + + /** + * Delay the execution of a callback. + * + * The delay is a minimum and approximate, accuracy is not guaranteed. Order of calls MUST be determined by which + * timers expire first, but timers with the same expiration time MAY be executed in any order. + * + * The created callback MUST immediately be marked as enabled, but only be activated (i.e. callback can be called) + * right before the next tick. Callbacks MUST NOT be called in the tick they were enabled. + * + * @param float $delay The amount of time, in seconds, to delay the execution for. + * @param \Closure(string):void $closure The callback to delay. The `$callbackId` will be invalidated + * before the callback invocation. + * + * @return string A unique identifier that can be used to cancel, enable or disable the callback. + */ + public static function delay(float $delay, \Closure $closure) : string + { + return self::getDriver()->delay($delay, $closure); + } + + /** + * Repeatedly execute a callback. + * + * The interval between executions is a minimum and approximate, accuracy is not guaranteed. Order of calls MUST be + * determined by which timers expire first, but timers with the same expiration time MAY be executed in any order. + * The first execution is scheduled after the first interval period. + * + * The created callback MUST immediately be marked as enabled, but only be activated (i.e. callback can be called) + * right before the next tick. Callbacks MUST NOT be called in the tick they were enabled. + * + * @param float $interval The time interval, in seconds, to wait between executions. + * @param \Closure(string):void $closure The callback to repeat. + * + * @return string A unique identifier that can be used to cancel, enable or disable the callback. + */ + public static function repeat(float $interval, \Closure $closure) : string + { + return self::getDriver()->repeat($interval, $closure); + } + + /** + * Execute a callback when a stream resource becomes readable or is closed for reading. + * + * Warning: Closing resources locally, e.g. with `fclose`, might not invoke the callback. Be sure to `cancel` the + * callback when closing the resource locally. Drivers MAY choose to notify the user if there are callbacks on + * invalid resources, but are not required to, due to the high performance impact. Callbacks on closed resources are + * therefore undefined behavior. + * + * Multiple callbacks on the same stream MAY be executed in any order. + * + * The created callback MUST immediately be marked as enabled, but only be activated (i.e. callback can be called) + * right before the next tick. Callbacks MUST NOT be called in the tick they were enabled. + * + * @param resource $stream The stream to monitor. + * @param \Closure(string, resource):void $closure The callback to execute. + * + * @return string A unique identifier that can be used to cancel, enable or disable the callback. + */ + public static function onReadable(mixed $stream, \Closure $closure) : string + { + return self::getDriver()->onReadable($stream, $closure); + } + + /** + * Execute a callback when a stream resource becomes writable or is closed for writing. + * + * Warning: Closing resources locally, e.g. with `fclose`, might not invoke the callback. Be sure to `cancel` the + * callback when closing the resource locally. Drivers MAY choose to notify the user if there are callbacks on + * invalid resources, but are not required to, due to the high performance impact. Callbacks on closed resources are + * therefore undefined behavior. + * + * Multiple callbacks on the same stream MAY be executed in any order. + * + * The created callback MUST immediately be marked as enabled, but only be activated (i.e. callback can be called) + * right before the next tick. Callbacks MUST NOT be called in the tick they were enabled. + * + * @param resource $stream The stream to monitor. + * @param \Closure(string, resource):void $closure The callback to execute. + * + * @return string A unique identifier that can be used to cancel, enable or disable the callback. + */ + public static function onWritable(mixed $stream, \Closure $closure) : string + { + return self::getDriver()->onWritable($stream, $closure); + } + + /** + * Execute a callback when a signal is received. + * + * Warning: Installing the same signal on different instances of this interface is deemed undefined behavior. + * Implementations MAY try to detect this, if possible, but are not required to. This is due to technical + * limitations of the signals being registered globally per process. + * + * Multiple callbacks on the same signal MAY be executed in any order. + * + * The created callback MUST immediately be marked as enabled, but only be activated (i.e. callback can be called) + * right before the next tick. Callbacks MUST NOT be called in the tick they were enabled. + * + * @param int $signal The signal number to monitor. + * @param \Closure(string, int):void $closure The callback to execute. + * + * @return string A unique identifier that can be used to cancel, enable or disable the callback. + * + * @throws UnsupportedFeatureException If signal handling is not supported. + */ + public static function onSignal(int $signal, \Closure $closure) : string + { + return self::getDriver()->onSignal($signal, $closure); + } + + /** + * Enable a callback to be active starting in the next tick. + * + * Callbacks MUST immediately be marked as enabled, but only be activated (i.e. callbacks can be called) right + * before the next tick. Callbacks MUST NOT be called in the tick they were enabled. + * + * @param string $callbackId The callback identifier. + * + * @return string The callback identifier. + * + * @throws InvalidCallbackError If the callback identifier is invalid. + */ + public static function enable(string $callbackId) : string + { + return self::getDriver()->enable($callbackId); + } + + /** + * Disable a callback immediately. + * + * A callback MUST be disabled immediately, e.g. if a deferred callback disables another deferred callback, + * the second deferred callback isn't executed in this tick. + * + * Disabling a callback MUST NOT invalidate the callback. Calling this function MUST NOT fail, even if passed an + * invalid callback identifier. + * + * @param string $callbackId The callback identifier. + * + * @return string The callback identifier. + */ + public static function disable(string $callbackId) : string + { + return self::getDriver()->disable($callbackId); + } + + /** + * Cancel a callback. + * + * This will detach the event loop from all resources that are associated to the callback. After this operation the + * callback is permanently invalid. Calling this function MUST NOT fail, even if passed an invalid identifier. + * + * @param string $callbackId The callback identifier. + */ + public static function cancel(string $callbackId) : void + { + self::getDriver()->cancel($callbackId); + } + + /** + * Reference a callback. + * + * This will keep the event loop alive whilst the event is still being monitored. Callbacks have this state by + * default. + * + * @param string $callbackId The callback identifier. + * + * @return string The callback identifier. + * + * @throws InvalidCallbackError If the callback identifier is invalid. + */ + public static function reference(string $callbackId) : string + { + return self::getDriver()->reference($callbackId); + } + + /** + * Unreference a callback. + * + * The event loop should exit the run method when only unreferenced callbacks are still being monitored. Callbacks + * are all referenced by default. + * + * @param string $callbackId The callback identifier. + * + * @return string The callback identifier. + */ + public static function unreference(string $callbackId) : string + { + return self::getDriver()->unreference($callbackId); + } + + /** + * Set a callback to be executed when an error occurs. + * + * The callback receives the error as the first and only parameter. The return value of the callback gets ignored. + * If it can't handle the error, it MUST throw the error. Errors thrown by the callback or during its invocation + * MUST be thrown into the `run` loop and stop the driver. + * + * Subsequent calls to this method will overwrite the previous handler. + * + * @param null|\Closure(\Throwable):void $errorHandler The callback to execute. `null` will clear the current handler. + */ + public static function setErrorHandler(?\Closure $errorHandler) : void + { + self::getDriver()->setErrorHandler($errorHandler); + } + + /** + * Gets the error handler closure or {@code null} if none is set. + * + * @return null|\Closure(\Throwable):void The previous handler, `null` if there was none. + */ + public static function getErrorHandler() : ?\Closure + { + return self::getDriver()->getErrorHandler(); + } + + /** + * Returns all registered non-cancelled callback identifiers. + * + * @return string[] Callback identifiers. + */ + public static function getIdentifiers() : array + { + return self::getDriver()->getIdentifiers(); + } + + /** + * Returns the type of the callback identified by the given callback identifier. + * + * @param string $callbackId The callback identifier. + * + * @return CallbackType The callback type. + */ + public static function getType(string $callbackId) : CallbackType + { + return self::getDriver()->getType($callbackId); + } + + /** + * Returns whether the callback identified by the given callback identifier is currently enabled. + * + * @param string $callbackId The callback identifier. + * + * @return bool {@code true} if the callback is currently enabled, otherwise {@code false}. + */ + public static function isEnabled(string $callbackId) : bool + { + return self::getDriver()->isEnabled($callbackId); + } + + /** + * Returns whether the callback identified by the given callback identifier is currently referenced. + * + * @param string $callbackId The callback identifier. + * + * @return bool {@code true} if the callback is currently referenced, otherwise {@code false}. + */ + public static function isReferenced(string $callbackId) : bool + { + return self::getDriver()->isReferenced($callbackId); + } + + /** + * Retrieve the event loop driver that is in scope. + */ + public static function getDriver() : Driver + { + /** @psalm-suppress RedundantPropertyInitializationCheck, RedundantCondition */ + if (!isset(self::$driver)) { + self::setDriver((new DriverFactory())->create()); + } + + return self::$driver; + } + + /** + * Returns an object used to suspend and resume execution of the current fiber or {main}. + * + * Calls from the same fiber will return the same suspension object. + */ + public static function getSuspension() : Suspension + { + return self::getDriver()->getSuspension(); + } + + /** + * Run the event loop. + * + * This function may only be called from {main}, that is, not within a fiber. + * + * Libraries should use the {@link Suspension} API instead of calling this method. + * + * This method will not return until the event loop does not contain any pending, referenced callbacks anymore. + */ + public static function run() : void + { + self::getDriver()->run(); + } + + /** + * Disable construction as this is a static class. + */ + private function __construct() + { + // intentionally left blank + } +} diff --git a/src/thebigcrafter/Hydrogen/eventLoop/CallbackType.php b/src/thebigcrafter/Hydrogen/eventLoop/CallbackType.php new file mode 100644 index 0000000..0765aaa --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/CallbackType.php @@ -0,0 +1,22 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +namespace thebigcrafter\Hydrogen\eventLoop; + +enum CallbackType +{ + case Defer; + case Delay; + case Repeat; + case Readable; + case Writable; + case Signal; +} diff --git a/src/thebigcrafter/Hydrogen/eventLoop/Driver.php b/src/thebigcrafter/Hydrogen/eventLoop/Driver.php new file mode 100644 index 0000000..f9d36e0 --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/Driver.php @@ -0,0 +1,321 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +namespace thebigcrafter\Hydrogen\eventLoop; + +/** + * The driver MUST run in its own fiber and execute callbacks in a separate fiber. If fibers are reused, the driver + * needs to call {@see FiberLocal::clear()} after running the callback. + */ +interface Driver +{ + /** + * Run the event loop. + * + * One iteration of the loop is called one "tick". A tick covers the following steps: + * + * 1. Activate callbacks created / enabled in the last tick / before `run()`. + * 2. Execute all enabled deferred callbacks. + * 3. Execute all due timer, pending signal and actionable stream callbacks, each only once per tick. + * + * The loop MUST continue to run until it is either stopped explicitly, no referenced callbacks exist anymore, or an + * exception is thrown that cannot be handled. Exceptions that cannot be handled are exceptions thrown from an + * error handler or exceptions that would be passed to an error handler but none exists to handle them. + * + * @throws \Error Thrown if the event loop is already running. + */ + public function run() : void; + + /** + * Stop the event loop. + * + * When an event loop is stopped, it continues with its current tick and exits the loop afterwards. Multiple calls + * to stop MUST be ignored and MUST NOT raise an exception. + */ + public function stop() : void; + + /** + * Returns an object used to suspend and resume execution of the current fiber or {main}. + * + * Calls from the same fiber will return the same suspension object. + */ + public function getSuspension() : Suspension; + + /** + * @return bool True if the event loop is running, false if it is stopped. + */ + public function isRunning() : bool; + + /** + * Queue a microtask. + * + * The queued callback MUST be executed immediately once the event loop gains control. Order of queueing MUST be + * preserved when executing the callbacks. Recursive scheduling can thus result in infinite loops, use with care. + * + * Does NOT create an event callback, thus CAN NOT be marked as disabled or unreferenced. + * Use {@see EventLoop::defer()} if you need these features. + * + * @param \Closure(...):void $closure The callback to queue. + * @param mixed ...$args The callback arguments. + */ + public function queue(\Closure $closure, mixed ...$args) : void; + + /** + * Defer the execution of a callback. + * + * The deferred callback MUST be executed before any other type of callback in a tick. Order of enabling MUST be + * preserved when executing the callbacks. + * + * The created callback MUST immediately be marked as enabled, but only be activated (i.e. callback can be called) + * right before the next tick. Callbacks MUST NOT be called in the tick they were enabled. + * + * @param \Closure(string):void $closure The callback to defer. The `$callbackId` will be invalidated before the + * callback invocation. + * + * @return string A unique identifier that can be used to cancel, enable or disable the callback. + */ + public function defer(\Closure $closure) : string; + + /** + * Delay the execution of a callback. + * + * The delay is a minimum and approximate, accuracy is not guaranteed. Order of calls MUST be determined by which + * timers expire first, but timers with the same expiration time MAY be executed in any order. + * + * The created callback MUST immediately be marked as enabled, but only be activated (i.e. callback can be called) + * right before the next tick. Callbacks MUST NOT be called in the tick they were enabled. + * + * @param float $delay The amount of time, in seconds, to delay the execution for. + * @param \Closure(string):void $closure The callback to delay. The `$callbackId` will be invalidated before the + * callback invocation. + * + * @return string A unique identifier that can be used to cancel, enable or disable the callback. + */ + public function delay(float $delay, \Closure $closure) : string; + + /** + * Repeatedly execute a callback. + * + * The interval between executions is a minimum and approximate, accuracy is not guaranteed. Order of calls MUST be + * determined by which timers expire first, but timers with the same expiration time MAY be executed in any order. + * The first execution is scheduled after the first interval period. + * + * The created callback MUST immediately be marked as enabled, but only be activated (i.e. callback can be called) + * right before the next tick. Callbacks MUST NOT be called in the tick they were enabled. + * + * @param float $interval The time interval, in seconds, to wait between executions. + * @param \Closure(string):void $closure The callback to repeat. + * + * @return string A unique identifier that can be used to cancel, enable or disable the callback. + */ + public function repeat(float $interval, \Closure $closure) : string; + + /** + * Execute a callback when a stream resource becomes readable or is closed for reading. + * + * Warning: Closing resources locally, e.g. with `fclose`, might not invoke the callback. Be sure to `cancel` the + * callback when closing the resource locally. Drivers MAY choose to notify the user if there are callbacks on + * invalid resources, but are not required to, due to the high performance impact. Callbacks on closed resources are + * therefore undefined behavior. + * + * Multiple callbacks on the same stream MAY be executed in any order. + * + * The created callback MUST immediately be marked as enabled, but only be activated (i.e. callback can be called) + * right before the next tick. Callbacks MUST NOT be called in the tick they were enabled. + * + * @param resource $stream The stream to monitor. + * @param \Closure(string, resource):void $closure The callback to execute. + * + * @return string A unique identifier that can be used to cancel, enable or disable the callback. + */ + public function onReadable(mixed $stream, \Closure $closure) : string; + + /** + * Execute a callback when a stream resource becomes writable or is closed for writing. + * + * Warning: Closing resources locally, e.g. with `fclose`, might not invoke the callback. Be sure to `cancel` the + * callback when closing the resource locally. Drivers MAY choose to notify the user if there are callbacks on + * invalid resources, but are not required to, due to the high performance impact. Callbacks on closed resources are + * therefore undefined behavior. + * + * Multiple callbacks on the same stream MAY be executed in any order. + * + * The created callback MUST immediately be marked as enabled, but only be activated (i.e. callback can be called) + * right before the next tick. Callbacks MUST NOT be called in the tick they were enabled. + * + * @param resource $stream The stream to monitor. + * @param \Closure(string, resource):void $closure The callback to execute. + * + * @return string A unique identifier that can be used to cancel, enable or disable the callback. + */ + public function onWritable(mixed $stream, \Closure $closure) : string; + + /** + * Execute a callback when a signal is received. + * + * Warning: Installing the same signal on different instances of this interface is deemed undefined behavior. + * Implementations MAY try to detect this, if possible, but are not required to. This is due to technical + * limitations of the signals being registered globally per process. + * + * Multiple callbacks on the same signal MAY be executed in any order. + * + * The created callback MUST immediately be marked as enabled, but only be activated (i.e. callback can be called) + * right before the next tick. Callbacks MUST NOT be called in the tick they were enabled. + * + * @param int $signal The signal number to monitor. + * @param \Closure(string, int):void $closure The callback to execute. + * + * @return string A unique identifier that can be used to cancel, enable or disable the callback. + * + * @throws UnsupportedFeatureException If signal handling is not supported. + */ + public function onSignal(int $signal, \Closure $closure) : string; + + /** + * Enable a callback to be active starting in the next tick. + * + * Callbacks MUST immediately be marked as enabled, but only be activated (i.e. callbacks can be called) right + * before the next tick. Callbacks MUST NOT be called in the tick they were enabled. + * + * @param string $callbackId The callback identifier. + * + * @return string The callback identifier. + * + * @throws InvalidCallbackError If the callback identifier is invalid. + */ + public function enable(string $callbackId) : string; + + /** + * Cancel a callback. + * + * This will detach the event loop from all resources that are associated to the callback. After this operation the + * callback is permanently invalid. Calling this function MUST NOT fail, even if passed an invalid identifier. + * + * @param string $callbackId The callback identifier. + */ + public function cancel(string $callbackId) : void; + + /** + * Disable a callback immediately. + * + * A callback MUST be disabled immediately, e.g. if a deferred callback disables a later deferred callback, + * the second deferred callback isn't executed in this tick. + * + * Disabling a callback MUST NOT invalidate the callback. Calling this function MUST NOT fail, even if passed an + * invalid callback identifier. + * + * @param string $callbackId The callback identifier. + * + * @return string The callback identifier. + */ + public function disable(string $callbackId) : string; + + /** + * Reference a callback. + * + * This will keep the event loop alive whilst the callback is still being monitored. Callbacks have this state by + * default. + * + * @param string $callbackId The callback identifier. + * + * @return string The callback identifier. + * + * @throws InvalidCallbackError If the callback identifier is invalid. + */ + public function reference(string $callbackId) : string; + + /** + * Unreference a callback. + * + * The event loop should exit the run method when only unreferenced callbacks are still being monitored. Callbacks + * are all referenced by default. + * + * @param string $callbackId The callback identifier. + * + * @return string The callback identifier. + */ + public function unreference(string $callbackId) : string; + + /** + * Set a callback to be executed when an error occurs. + * + * The callback receives the error as the first and only parameter. The return value of the callback gets ignored. + * If it can't handle the error, it MUST throw the error. Errors thrown by the callback or during its invocation + * MUST be thrown into the `run` loop and stop the driver. + * + * Subsequent calls to this method will overwrite the previous handler. + * + * @param null|\Closure(\Throwable):void $errorHandler The callback to execute. `null` will clear the current + * handler. + */ + public function setErrorHandler(?\Closure $errorHandler) : void; + + /** + * Gets the error handler closure or {@code null} if none is set. + * + * @return null|\Closure(\Throwable):void The previous handler, `null` if there was none. + */ + public function getErrorHandler() : ?\Closure; + + /** + * Get the underlying loop handle. + * + * Example: the `uv_loop` resource for `libuv` or the `EvLoop` object for `libev` or `null` for a stream_select + * driver. + * + * Note: This function is *not* exposed in the `Loop` class. Users shall access it directly on the respective loop + * instance. + * + * @return null|object|resource The loop handle the event loop operates on. `null` if there is none. + */ + public function getHandle() : mixed; + + /** + * Returns all registered non-cancelled callback identifiers. + * + * @return string[] Callback identifiers. + */ + public function getIdentifiers() : array; + + /** + * Returns the type of the callback identified by the given callback identifier. + * + * @param string $callbackId The callback identifier. + * + * @return CallbackType The callback type. + */ + public function getType(string $callbackId) : CallbackType; + + /** + * Returns whether the callback identified by the given callback identifier is currently enabled. + * + * @param string $callbackId The callback identifier. + * + * @return bool {@code true} if the callback is currently enabled, otherwise {@code false}. + */ + public function isEnabled(string $callbackId) : bool; + + /** + * Returns whether the callback identified by the given callback identifier is currently referenced. + * + * @param string $callbackId The callback identifier. + * + * @return bool {@code true} if the callback is currently referenced, otherwise {@code false}. + */ + public function isReferenced(string $callbackId) : bool; + + /** + * Returns some useful information about the event loop. + * + * If this method isn't implemented, dumping the event loop in a busy application, even indirectly, is a pain. + */ + public function __debugInfo() : array; +} diff --git a/src/thebigcrafter/Hydrogen/eventLoop/Driver/EvDriver.php b/src/thebigcrafter/Hydrogen/eventLoop/Driver/EvDriver.php new file mode 100644 index 0000000..1676abe --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/Driver/EvDriver.php @@ -0,0 +1,232 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +/** @noinspection PhpComposerExtensionStubsInspection */ + +namespace thebigcrafter\Hydrogen\eventLoop\Driver; + +use thebigcrafter\Hydrogen\eventLoop\Internal\AbstractDriver; +use thebigcrafter\Hydrogen\eventLoop\Internal\DriverCallback; +use thebigcrafter\Hydrogen\eventLoop\Internal\SignalCallback; +use thebigcrafter\Hydrogen\eventLoop\Internal\StreamCallback; +use thebigcrafter\Hydrogen\eventLoop\Internal\StreamReadableCallback; +use thebigcrafter\Hydrogen\eventLoop\Internal\StreamWritableCallback; +use thebigcrafter\Hydrogen\eventLoop\Internal\TimerCallback; +use function assert; +use function extension_loaded; +use function get_class; +use function hrtime; +use function is_resource; +use function max; + +final class EvDriver extends AbstractDriver +{ + /** @var array|null */ + private static ?array $activeSignals = null; + + public static function isSupported() : bool + { + return extension_loaded("ev"); + } + + private \EvLoop $handle; + + /** @var array */ + private array $events = []; + + private readonly \Closure $ioCallback; + + private readonly \Closure $timerCallback; + + private readonly \Closure $signalCallback; + + /** @var array */ + private array $signals = []; + + public function __construct() + { + parent::__construct(); + + $this->handle = new \EvLoop(); + + if (self::$activeSignals === null) { + self::$activeSignals = &$this->signals; + } + + $this->ioCallback = function (\EvIo $event) : void { + /** @var StreamCallback $callback */ + $callback = $event->data; + + $this->enqueueCallback($callback); + }; + + $this->timerCallback = function (\EvTimer $event) : void { + /** @var TimerCallback $callback */ + $callback = $event->data; + + $this->enqueueCallback($callback); + }; + + $this->signalCallback = function (\EvSignal $event) : void { + /** @var SignalCallback $callback */ + $callback = $event->data; + + $this->enqueueCallback($callback); + }; + } + + /** + * {@inheritdoc} + */ + public function cancel(string $callbackId) : void + { + parent::cancel($callbackId); + unset($this->events[$callbackId]); + } + + public function __destruct() + { + foreach ($this->events as $event) { + /** @psalm-suppress all */ + if ($event !== null) { // Events may have been nulled in extension depending on destruct order. + $event->stop(); + } + } + + // We need to clear all references to events manually, see + // https://bitbucket.org/osmanov/pecl-ev/issues/31/segfault-in-ev_timer_stop + $this->events = []; + } + + /** + * {@inheritdoc} + */ + public function run() : void + { + $active = self::$activeSignals; + + assert($active !== null); + + foreach ($active as $event) { + $event->stop(); + } + + self::$activeSignals = &$this->signals; + + foreach ($this->signals as $event) { + $event->start(); + } + + try { + parent::run(); + } finally { + foreach ($this->signals as $event) { + $event->stop(); + } + + self::$activeSignals = &$active; + + foreach ($active as $event) { + $event->start(); + } + } + } + + /** + * {@inheritdoc} + */ + public function stop() : void + { + $this->handle->stop(); + parent::stop(); + } + + /** + * {@inheritdoc} + */ + public function getHandle() : \EvLoop + { + return $this->handle; + } + + protected function now() : float + { + return (float) hrtime(true) / 1_000_000_000; + } + + /** + * {@inheritdoc} + */ + protected function dispatch(bool $blocking) : void + { + $this->handle->run($blocking ? \Ev::RUN_ONCE : \Ev::RUN_ONCE | \Ev::RUN_NOWAIT); + } + + /** + * {@inheritdoc} + */ + protected function activate(array $callbacks) : void + { + $this->handle->nowUpdate(); + $now = $this->now(); + + foreach ($callbacks as $callback) { + if (!isset($this->events[$id = $callback->id])) { + if ($callback instanceof StreamReadableCallback) { + assert(is_resource($callback->stream)); + + $this->events[$id] = $this->handle->io($callback->stream, \Ev::READ, $this->ioCallback, $callback); + } elseif ($callback instanceof StreamWritableCallback) { + assert(is_resource($callback->stream)); + + $this->events[$id] = $this->handle->io( + $callback->stream, + \Ev::WRITE, + $this->ioCallback, + $callback + ); + } elseif ($callback instanceof TimerCallback) { + $interval = $callback->interval; + $this->events[$id] = $this->handle->timer( + max(0, ($callback->expiration - $now)), + $callback->repeat ? $interval : 0, + $this->timerCallback, + $callback + ); + } elseif ($callback instanceof SignalCallback) { + $this->events[$id] = $this->handle->signal($callback->signal, $this->signalCallback, $callback); + } else { + // @codeCoverageIgnoreStart + throw new \Error("Unknown callback type: " . get_class($callback)); + // @codeCoverageIgnoreEnd + } + } else { + $this->events[$id]->start(); + } + + if ($callback instanceof SignalCallback) { + /** @psalm-suppress PropertyTypeCoercion */ + $this->signals[$id] = $this->events[$id]; + } + } + } + + protected function deactivate(DriverCallback $callback) : void + { + if (isset($this->events[$id = $callback->id])) { + $this->events[$id]->stop(); + + if ($callback instanceof SignalCallback) { + unset($this->signals[$id]); + } + } + } +} diff --git a/src/thebigcrafter/Hydrogen/eventLoop/Driver/EventDriver.php b/src/thebigcrafter/Hydrogen/eventLoop/Driver/EventDriver.php new file mode 100644 index 0000000..be182ba --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/Driver/EventDriver.php @@ -0,0 +1,257 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +/** @noinspection PhpComposerExtensionStubsInspection */ + +namespace thebigcrafter\Hydrogen\eventLoop\Driver; + +use thebigcrafter\Hydrogen\eventLoop\Internal\AbstractDriver; +use thebigcrafter\Hydrogen\eventLoop\Internal\DriverCallback; +use thebigcrafter\Hydrogen\eventLoop\Internal\SignalCallback; +use thebigcrafter\Hydrogen\eventLoop\Internal\StreamCallback; +use thebigcrafter\Hydrogen\eventLoop\Internal\StreamReadableCallback; +use thebigcrafter\Hydrogen\eventLoop\Internal\StreamWritableCallback; +use thebigcrafter\Hydrogen\eventLoop\Internal\TimerCallback; +use function assert; +use function extension_loaded; +use function hrtime; +use function is_resource; +use function max; +use function min; +use const PHP_INT_MAX; + +final class EventDriver extends AbstractDriver +{ + /** @var array|null */ + private static ?array $activeSignals = null; + + public static function isSupported() : bool + { + return extension_loaded("event"); + } + + private \EventBase $handle; + /** @var array */ + private array $events = []; + private readonly \Closure $ioCallback; + private readonly \Closure $timerCallback; + private readonly \Closure $signalCallback; + + /** @var array */ + private array $signals = []; + + public function __construct() + { + parent::__construct(); + + /** @psalm-suppress TooFewArguments https://github.com/JetBrains/phpstorm-stubs/pull/763 */ + $this->handle = new \EventBase(); + + if (self::$activeSignals === null) { + self::$activeSignals = &$this->signals; + } + + $this->ioCallback = function ($resource, $what, StreamCallback $callback) : void { + $this->enqueueCallback($callback); + }; + + $this->timerCallback = function ($resource, $what, TimerCallback $callback) : void { + $this->enqueueCallback($callback); + }; + + $this->signalCallback = function ($signo, $what, SignalCallback $callback) : void { + $this->enqueueCallback($callback); + }; + } + + /** + * {@inheritdoc} + */ + public function cancel(string $callbackId) : void + { + parent::cancel($callbackId); + + if (isset($this->events[$callbackId])) { + $this->events[$callbackId]->free(); + unset($this->events[$callbackId]); + } + } + + /** + * @codeCoverageIgnore + */ + public function __destruct() + { + foreach ($this->events as $event) { + if ($event !== null) { // Events may have been nulled in extension depending on destruct order. + $event->free(); + } + } + + // Unset here, otherwise $event->del() fails with a warning, because __destruct order isn't defined. + // See https://github.com/amphp/amp/issues/159. + $this->events = []; + + // Manually free the loop handle to fully release loop resources. + // See https://github.com/amphp/amp/issues/177. + /** @psalm-suppress RedundantPropertyInitializationCheck */ + if (isset($this->handle)) { + $this->handle->free(); + unset($this->handle); + } + } + + /** + * {@inheritdoc} + */ + public function run() : void + { + $active = self::$activeSignals; + + assert($active !== null); + + foreach ($active as $event) { + $event->del(); + } + + self::$activeSignals = &$this->signals; + + foreach ($this->signals as $event) { + /** @psalm-suppress TooFewArguments https://github.com/JetBrains/phpstorm-stubs/pull/763 */ + $event->add(); + } + + try { + parent::run(); + } finally { + foreach ($this->signals as $event) { + $event->del(); + } + + self::$activeSignals = &$active; + + foreach ($active as $event) { + /** @psalm-suppress TooFewArguments https://github.com/JetBrains/phpstorm-stubs/pull/763 */ + $event->add(); + } + } + } + + /** + * {@inheritdoc} + */ + public function stop() : void + { + $this->handle->stop(); + parent::stop(); + } + + /** + * {@inheritdoc} + */ + public function getHandle() : \EventBase + { + return $this->handle; + } + + protected function now() : float + { + return (float) hrtime(true) / 1_000_000_000; + } + + /** + * {@inheritdoc} + */ + protected function dispatch(bool $blocking) : void + { + $this->handle->loop($blocking ? \EventBase::LOOP_ONCE : \EventBase::LOOP_ONCE | \EventBase::LOOP_NONBLOCK); + } + + /** + * {@inheritdoc} + */ + protected function activate(array $callbacks) : void + { + $now = $this->now(); + + foreach ($callbacks as $callback) { + if (!isset($this->events[$id = $callback->id])) { + if ($callback instanceof StreamReadableCallback) { + assert(is_resource($callback->stream)); + + $this->events[$id] = new \Event( + $this->handle, + $callback->stream, + \Event::READ | \Event::PERSIST, + $this->ioCallback, + $callback + ); + } elseif ($callback instanceof StreamWritableCallback) { + assert(is_resource($callback->stream)); + + $this->events[$id] = new \Event( + $this->handle, + $callback->stream, + \Event::WRITE | \Event::PERSIST, + $this->ioCallback, + $callback + ); + } elseif ($callback instanceof TimerCallback) { + $this->events[$id] = new \Event( + $this->handle, + -1, + \Event::TIMEOUT, + $this->timerCallback, + $callback + ); + } elseif ($callback instanceof SignalCallback) { + $this->events[$id] = new \Event( + $this->handle, + $callback->signal, + \Event::SIGNAL | \Event::PERSIST, + $this->signalCallback, + $callback + ); + } else { + // @codeCoverageIgnoreStart + throw new \Error("Unknown callback type"); + // @codeCoverageIgnoreEnd + } + } + + if ($callback instanceof TimerCallback) { + $interval = min(max(0, $callback->expiration - $now), PHP_INT_MAX / 2); + $this->events[$id]->add($interval > 0 ? $interval : 0); + } elseif ($callback instanceof SignalCallback) { + $this->signals[$id] = $this->events[$id]; + /** @psalm-suppress TooFewArguments https://github.com/JetBrains/phpstorm-stubs/pull/763 */ + $this->events[$id]->add(); + } else { + /** @psalm-suppress TooFewArguments https://github.com/JetBrains/phpstorm-stubs/pull/763 */ + $this->events[$id]->add(); + } + } + } + + /** + * {@inheritdoc} + */ + protected function deactivate(DriverCallback $callback) : void + { + if (isset($this->events[$id = $callback->id])) { + $this->events[$id]->del(); + + if ($callback instanceof SignalCallback) { + unset($this->signals[$id]); + } + } + } +} diff --git a/src/thebigcrafter/Hydrogen/eventLoop/Driver/StreamSelectDriver.php b/src/thebigcrafter/Hydrogen/eventLoop/Driver/StreamSelectDriver.php new file mode 100644 index 0000000..3970f35 --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/Driver/StreamSelectDriver.php @@ -0,0 +1,358 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +/** @noinspection PhpComposerExtensionStubsInspection */ + +namespace thebigcrafter\Hydrogen\eventLoop\Driver; + +use thebigcrafter\Hydrogen\eventLoop\Internal\AbstractDriver; +use thebigcrafter\Hydrogen\eventLoop\Internal\DriverCallback; +use thebigcrafter\Hydrogen\eventLoop\Internal\SignalCallback; +use thebigcrafter\Hydrogen\eventLoop\Internal\StreamReadableCallback; +use thebigcrafter\Hydrogen\eventLoop\Internal\StreamWritableCallback; +use thebigcrafter\Hydrogen\eventLoop\Internal\TimerCallback; +use thebigcrafter\Hydrogen\eventLoop\Internal\TimerQueue; +use thebigcrafter\Hydrogen\eventLoop\UnsupportedFeatureException; +use function assert; +use function extension_loaded; +use function function_exists; +use function hrtime; +use function is_resource; +use function pcntl_signal; +use function pcntl_signal_dispatch; +use function preg_match; +use function restore_error_handler; +use function set_error_handler; +use function sprintf; +use function str_contains; +use function str_replace; +use function stream_select; +use function stripos; +use function usleep; +use const DIRECTORY_SEPARATOR; +use const PHP_INT_MAX; + +final class StreamSelectDriver extends AbstractDriver +{ + /** @var array */ + private array $readStreams = []; + + /** @var array> */ + private array $readCallbacks = []; + + /** @var array */ + private array $writeStreams = []; + + /** @var array> */ + private array $writeCallbacks = []; + + private readonly TimerQueue $timerQueue; + + /** @var array> */ + private array $signalCallbacks = []; + + /** @var \SplQueue */ + private readonly \SplQueue $signalQueue; + + private bool $signalHandling; + + private readonly \Closure $streamSelectErrorHandler; + + private bool $streamSelectIgnoreResult = false; + + public function __construct() + { + parent::__construct(); + + $this->signalQueue = new \SplQueue(); + $this->timerQueue = new TimerQueue(); + $this->signalHandling = extension_loaded("pcntl") + && function_exists('pcntl_signal_dispatch') + && function_exists('pcntl_signal'); + + $this->streamSelectErrorHandler = function (int $errno, string $message) : void { + // Casing changed in PHP 8 from 'unable' to 'Unable' + if (stripos($message, "stream_select(): unable to select [4]: ") === 0) { // EINTR + $this->streamSelectIgnoreResult = true; + + return; + } + + if (str_contains($message, 'FD_SETSIZE')) { + $message = str_replace(["\r\n", "\n", "\r"], " ", $message); + $pattern = '(stream_select\(\): You MUST recompile PHP with a larger value of FD_SETSIZE. It is set to (\d+), but you have descriptors numbered at least as high as (\d+)\.)'; + + if (preg_match($pattern, $message, $match)) { + $helpLink = 'https://revolt.run/extensions'; + + $message = 'You have reached the limits of stream_select(). It has a FD_SETSIZE of ' . $match[1] + . ', but you have file descriptors numbered at least as high as ' . $match[2] . '. ' + . "You can install one of the extensions listed on {$helpLink} to support a higher number of " + . "concurrent file descriptors. If a large number of open file descriptors is unexpected, you " + . "might be leaking file descriptors that aren't closed correctly."; + } + } + + throw new \Exception($message, $errno); + }; + } + + public function __destruct() + { + foreach ($this->signalCallbacks as $signalCallbacks) { + foreach ($signalCallbacks as $signalCallback) { + $this->deactivate($signalCallback); + } + } + } + + /** + * @throws UnsupportedFeatureException If the pcntl extension is not available. + */ + public function onSignal(int $signal, \Closure $closure) : string + { + if (!$this->signalHandling) { + throw new UnsupportedFeatureException("Signal handling requires the pcntl extension"); + } + + return parent::onSignal($signal, $closure); + } + + public function getHandle() : mixed + { + return null; + } + + protected function now() : float + { + return (float) hrtime(true) / 1_000_000_000; + } + + /** + * @throws \Throwable + */ + protected function dispatch(bool $blocking) : void + { + if ($this->signalHandling) { + pcntl_signal_dispatch(); + + while (!$this->signalQueue->isEmpty()) { + $signal = $this->signalQueue->dequeue(); + + foreach ($this->signalCallbacks[$signal] as $callback) { + $this->enqueueCallback($callback); + } + + $blocking = false; + } + } + + $this->selectStreams( + $this->readStreams, + $this->writeStreams, + $blocking ? $this->getTimeout() : 0.0 + ); + + $now = $this->now(); + + while ($callback = $this->timerQueue->extract($now)) { + $this->enqueueCallback($callback); + } + } + + protected function activate(array $callbacks) : void + { + foreach ($callbacks as $callback) { + if ($callback instanceof StreamReadableCallback) { + assert(is_resource($callback->stream)); + + $streamId = (int) $callback->stream; + $this->readCallbacks[$streamId][$callback->id] = $callback; + $this->readStreams[$streamId] = $callback->stream; + } elseif ($callback instanceof StreamWritableCallback) { + assert(is_resource($callback->stream)); + + $streamId = (int) $callback->stream; + $this->writeCallbacks[$streamId][$callback->id] = $callback; + $this->writeStreams[$streamId] = $callback->stream; + } elseif ($callback instanceof TimerCallback) { + $this->timerQueue->insert($callback); + } elseif ($callback instanceof SignalCallback) { + if (!isset($this->signalCallbacks[$callback->signal])) { + set_error_handler(static function (int $errno, string $errstr) : bool { + throw new UnsupportedFeatureException( + sprintf("Failed to register signal handler; Errno: %d; %s", $errno, $errstr) + ); + }); + + // Avoid bug in Psalm handling of first-class callables by assigning to a temp variable. + $handler = $this->handleSignal(...); + + try { + pcntl_signal($callback->signal, $handler); + } finally { + restore_error_handler(); + } + } + + $this->signalCallbacks[$callback->signal][$callback->id] = $callback; + } else { + // @codeCoverageIgnoreStart + throw new \Error("Unknown callback type"); + // @codeCoverageIgnoreEnd + } + } + } + + protected function deactivate(DriverCallback $callback) : void + { + if ($callback instanceof StreamReadableCallback) { + $streamId = (int) $callback->stream; + unset($this->readCallbacks[$streamId][$callback->id]); + if (empty($this->readCallbacks[$streamId])) { + unset($this->readCallbacks[$streamId], $this->readStreams[$streamId]); + } + } elseif ($callback instanceof StreamWritableCallback) { + $streamId = (int) $callback->stream; + unset($this->writeCallbacks[$streamId][$callback->id]); + if (empty($this->writeCallbacks[$streamId])) { + unset($this->writeCallbacks[$streamId], $this->writeStreams[$streamId]); + } + } elseif ($callback instanceof TimerCallback) { + $this->timerQueue->remove($callback); + } elseif ($callback instanceof SignalCallback) { + if (isset($this->signalCallbacks[$callback->signal])) { + unset($this->signalCallbacks[$callback->signal][$callback->id]); + + if (empty($this->signalCallbacks[$callback->signal])) { + unset($this->signalCallbacks[$callback->signal]); + set_error_handler(static fn () => true); + try { + pcntl_signal($callback->signal, SIG_DFL); + } finally { + restore_error_handler(); + } + } + } + } else { + // @codeCoverageIgnoreStart + throw new \Error("Unknown callback type"); + // @codeCoverageIgnoreEnd + } + } + + /** + * @param array $read + * @param array $write + */ + private function selectStreams(array $read, array $write, float $timeout) : void + { + if (!empty($read) || !empty($write)) { // Use stream_select() if there are any streams in the loop. + if ($timeout >= 0) { + $seconds = (int) $timeout; + $microseconds = (int) (($timeout - $seconds) * 1_000_000); + } else { + $seconds = null; + $microseconds = null; + } + + // Failed connection attempts are indicated via except on Windows + // @link https://github.com/reactphp/event-loop/blob/8bd064ce23c26c4decf186c2a5a818c9a8209eb0/src/StreamSelectLoop.php#L279-L287 + // @link https://docs.microsoft.com/de-de/windows/win32/api/winsock2/nf-winsock2-select + $except = null; + if (DIRECTORY_SEPARATOR === '\\') { + $except = $write; + } + + set_error_handler($this->streamSelectErrorHandler); + + try { + /** @psalm-suppress InvalidArgument */ + $result = stream_select($read, $write, $except, $seconds, $microseconds); + } finally { + restore_error_handler(); + } + + if ($this->streamSelectIgnoreResult || $result === 0) { + $this->streamSelectIgnoreResult = false; + return; + } + + if (!$result) { + throw new \Exception('Unknown error during stream_select'); + } + + foreach ($read as $stream) { + $streamId = (int) $stream; + if (!isset($this->readCallbacks[$streamId])) { + continue; // All read callbacks disabled. + } + + foreach ($this->readCallbacks[$streamId] as $callback) { + $this->enqueueCallback($callback); + } + } + + /** @var array|null $except */ + if ($except) { + foreach ($except as $key => $socket) { + $write[$key] = $socket; + } + } + + foreach ($write as $stream) { + $streamId = (int) $stream; + if (!isset($this->writeCallbacks[$streamId])) { + continue; // All write callbacks disabled. + } + + foreach ($this->writeCallbacks[$streamId] as $callback) { + $this->enqueueCallback($callback); + } + } + + return; + } + + if ($timeout < 0) { // Only signal callbacks are enabled, so sleep indefinitely. + /** @psalm-suppress ArgumentTypeCoercion */ + usleep(PHP_INT_MAX); + return; + } + + if ($timeout > 0) { // Sleep until next timer expires. + /** @psalm-var positive-int $timeout */ + usleep((int) ($timeout * 1_000_000)); + } + } + + /** + * @return float Seconds until next timer expires or -1 if there are no pending timers. + */ + private function getTimeout() : float + { + $expiration = $this->timerQueue->peek(); + + if ($expiration === null) { + return -1; + } + + $expiration -= $this->now(); + + return $expiration > 0 ? $expiration : 0.0; + } + + private function handleSignal(int $signal) : void + { + // Queue signals, so we don't suspend inside pcntl_signal_dispatch, which disables signals while it runs + $this->signalQueue->enqueue($signal); + } +} diff --git a/src/thebigcrafter/Hydrogen/eventLoop/Driver/TracingDriver.php b/src/thebigcrafter/Hydrogen/eventLoop/Driver/TracingDriver.php new file mode 100644 index 0000000..449baac --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/Driver/TracingDriver.php @@ -0,0 +1,286 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +namespace thebigcrafter\Hydrogen\eventLoop\Driver; + +use thebigcrafter\Hydrogen\eventLoop\CallbackType; +use thebigcrafter\Hydrogen\eventLoop\Driver; +use thebigcrafter\Hydrogen\eventLoop\InvalidCallbackError; +use thebigcrafter\Hydrogen\eventLoop\Suspension; +use function array_keys; +use function array_map; +use function debug_backtrace; +use function implode; +use function rtrim; +use const DEBUG_BACKTRACE_IGNORE_ARGS; + +final class TracingDriver implements Driver +{ + private readonly Driver $driver; + + /** @var array */ + private array $enabledCallbacks = []; + + /** @var array */ + private array $unreferencedCallbacks = []; + + /** @var array */ + private array $creationTraces = []; + + /** @var array */ + private array $cancelTraces = []; + + public function __construct(Driver $driver) + { + $this->driver = $driver; + } + + public function run() : void + { + $this->driver->run(); + } + + public function stop() : void + { + $this->driver->stop(); + } + + public function getSuspension() : Suspension + { + return $this->driver->getSuspension(); + } + + public function isRunning() : bool + { + return $this->driver->isRunning(); + } + + public function defer(\Closure $closure) : string + { + $id = $this->driver->defer(function (...$args) use ($closure) { + $this->cancel($args[0]); + return $closure(...$args); + }); + + $this->creationTraces[$id] = $this->formatStacktrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)); + $this->enabledCallbacks[$id] = true; + + return $id; + } + + public function delay(float $delay, \Closure $closure) : string + { + $id = $this->driver->delay($delay, function (...$args) use ($closure) { + $this->cancel($args[0]); + return $closure(...$args); + }); + + $this->creationTraces[$id] = $this->formatStacktrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)); + $this->enabledCallbacks[$id] = true; + + return $id; + } + + public function repeat(float $interval, \Closure $closure) : string + { + $id = $this->driver->repeat($interval, $closure); + + $this->creationTraces[$id] = $this->formatStacktrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)); + $this->enabledCallbacks[$id] = true; + + return $id; + } + + public function onReadable(mixed $stream, \Closure $closure) : string + { + $id = $this->driver->onReadable($stream, $closure); + + $this->creationTraces[$id] = $this->formatStacktrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)); + $this->enabledCallbacks[$id] = true; + + return $id; + } + + public function onWritable(mixed $stream, \Closure $closure) : string + { + $id = $this->driver->onWritable($stream, $closure); + + $this->creationTraces[$id] = $this->formatStacktrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)); + $this->enabledCallbacks[$id] = true; + + return $id; + } + + public function onSignal(int $signal, \Closure $closure) : string + { + $id = $this->driver->onSignal($signal, $closure); + + $this->creationTraces[$id] = $this->formatStacktrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)); + $this->enabledCallbacks[$id] = true; + + return $id; + } + + public function enable(string $callbackId) : string + { + try { + $this->driver->enable($callbackId); + $this->enabledCallbacks[$callbackId] = true; + } catch (InvalidCallbackError $e) { + $e->addInfo("Creation trace", $this->getCreationTrace($callbackId)); + $e->addInfo("Cancellation trace", $this->getCancelTrace($callbackId)); + + throw $e; + } + + return $callbackId; + } + + public function cancel(string $callbackId) : void + { + $this->driver->cancel($callbackId); + + if (!isset($this->cancelTraces[$callbackId])) { + $this->cancelTraces[$callbackId] = $this->formatStacktrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)); + } + + unset($this->enabledCallbacks[$callbackId], $this->unreferencedCallbacks[$callbackId]); + } + + public function disable(string $callbackId) : string + { + $this->driver->disable($callbackId); + unset($this->enabledCallbacks[$callbackId]); + + return $callbackId; + } + + public function reference(string $callbackId) : string + { + try { + $this->driver->reference($callbackId); + unset($this->unreferencedCallbacks[$callbackId]); + } catch (InvalidCallbackError $e) { + $e->addInfo("Creation trace", $this->getCreationTrace($callbackId)); + $e->addInfo("Cancellation trace", $this->getCancelTrace($callbackId)); + + throw $e; + } + + return $callbackId; + } + + public function unreference(string $callbackId) : string + { + $this->driver->unreference($callbackId); + $this->unreferencedCallbacks[$callbackId] = true; + + return $callbackId; + } + + public function setErrorHandler(?\Closure $errorHandler) : void + { + $this->driver->setErrorHandler($errorHandler); + } + + public function getErrorHandler() : ?\Closure + { + return $this->driver->getErrorHandler(); + } + + /** @inheritdoc */ + public function getHandle() : mixed + { + return $this->driver->getHandle(); + } + + public function dump() : string + { + $dump = "Enabled, referenced callbacks keeping the loop running: "; + + foreach ($this->enabledCallbacks as $callbackId => $_) { + if (isset($this->unreferencedCallbacks[$callbackId])) { + continue; + } + + $dump .= "Callback identifier: " . $callbackId . "\r\n"; + $dump .= $this->getCreationTrace($callbackId); + $dump .= "\r\n\r\n"; + } + + return rtrim($dump); + } + + public function getIdentifiers() : array + { + return $this->driver->getIdentifiers(); + } + + public function getType(string $callbackId) : CallbackType + { + return $this->driver->getType($callbackId); + } + + public function isEnabled(string $callbackId) : bool + { + return $this->driver->isEnabled($callbackId); + } + + public function isReferenced(string $callbackId) : bool + { + return $this->driver->isReferenced($callbackId); + } + + public function __debugInfo() : array + { + return $this->driver->__debugInfo(); + } + + public function queue(\Closure $closure, mixed ...$args) : void + { + $this->driver->queue($closure, ...$args); + } + + private function getCreationTrace(string $callbackId) : string + { + return $this->creationTraces[$callbackId] ?? 'No creation trace, yet.'; + } + + private function getCancelTrace(string $callbackId) : string + { + return $this->cancelTraces[$callbackId] ?? 'No cancellation trace, yet.'; + } + + /** + * Formats a stacktrace obtained via `debug_backtrace()`. + * + * @param array $trace + * Output of `debug_backtrace()`. + * + * @return string Formatted stacktrace. + */ + private function formatStacktrace(array $trace) : string + { + return implode("\n", array_map(static function ($e, $i) { + $line = "#{$i} "; + + if (isset($e["file"])) { + $line .= "{$e['file']}:{$e['line']} "; + } + + if (isset($e["class"], $e["type"])) { + $line .= $e["class"] . $e["type"]; + } + + return $line . $e["function"] . "()"; + }, $trace, array_keys($trace))); + } +} diff --git a/src/thebigcrafter/Hydrogen/eventLoop/Driver/UvDriver.php b/src/thebigcrafter/Hydrogen/eventLoop/Driver/UvDriver.php new file mode 100644 index 0000000..273ad1b --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/Driver/UvDriver.php @@ -0,0 +1,289 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +namespace thebigcrafter\Hydrogen\eventLoop\Driver; + +use thebigcrafter\Hydrogen\eventLoop\Internal\AbstractDriver; +use thebigcrafter\Hydrogen\eventLoop\Internal\DriverCallback; +use thebigcrafter\Hydrogen\eventLoop\Internal\SignalCallback; +use thebigcrafter\Hydrogen\eventLoop\Internal\StreamCallback; +use thebigcrafter\Hydrogen\eventLoop\Internal\StreamReadableCallback; +use thebigcrafter\Hydrogen\eventLoop\Internal\StreamWritableCallback; +use thebigcrafter\Hydrogen\eventLoop\Internal\TimerCallback; +use function assert; +use function ceil; +use function extension_loaded; +use function is_resource; +use function max; +use function min; +use function uv_is_active; +use function uv_loop_new; +use function uv_now; +use function uv_poll_init_socket; +use function uv_poll_start; +use function uv_poll_stop; +use function uv_run; +use function uv_signal_init; +use function uv_signal_start; +use function uv_signal_stop; +use function uv_timer_init; +use function uv_timer_start; +use function uv_timer_stop; +use function uv_update_time; +use const PHP_INT_MAX; + +final class UvDriver extends AbstractDriver +{ + public static function isSupported() : bool + { + return extension_loaded("uv"); + } + + /** @var resource|\UVLoop A uv_loop resource created with uv_loop_new() */ + private $handle; + /** @var array */ + private array $events = []; + /** @var array> */ + private array $callbacks = []; + /** @var array */ + private array $streams = []; + private readonly \Closure $ioCallback; + private readonly \Closure $timerCallback; + private readonly \Closure $signalCallback; + + public function __construct() + { + parent::__construct(); + + $this->handle = uv_loop_new(); + + $this->ioCallback = function ($event, $status, $events, $resource) : void { + $callbacks = $this->callbacks[(int) $event]; + + // Invoke the callback on errors, as this matches behavior with other loop back-ends. + // Re-enable callback as libuv disables the callback on non-zero status. + if ($status !== 0) { + $flags = 0; + foreach ($callbacks as $callback) { + assert($callback instanceof StreamCallback); + + $flags |= $callback->invokable ? $this->getStreamCallbackFlags($callback) : 0; + } + uv_poll_start($event, $flags, $this->ioCallback); + } + + foreach ($callbacks as $callback) { + assert($callback instanceof StreamCallback); + + // $events is ORed with 4 to trigger callback if no events are indicated (0) or on UV_DISCONNECT (4). + // http://docs.libuv.org/en/v1.x/poll.html + if (!($this->getStreamCallbackFlags($callback) & $events || ($events | 4) === 4)) { + continue; + } + + $this->enqueueCallback($callback); + } + }; + + $this->timerCallback = function ($event) : void { + $callback = $this->callbacks[(int) $event][0]; + + assert($callback instanceof TimerCallback); + + $this->enqueueCallback($callback); + }; + + $this->signalCallback = function ($event) : void { + $callback = $this->callbacks[(int) $event][0]; + + $this->enqueueCallback($callback); + }; + } + + /** + * {@inheritdoc} + */ + public function cancel(string $callbackId) : void + { + parent::cancel($callbackId); + + if (!isset($this->events[$callbackId])) { + return; + } + + $event = $this->events[$callbackId]; + $eventId = (int) $event; + + if (isset($this->callbacks[$eventId][0])) { // All except IO callbacks. + unset($this->callbacks[$eventId]); + } elseif (isset($this->callbacks[$eventId][$callbackId])) { + $callback = $this->callbacks[$eventId][$callbackId]; + unset($this->callbacks[$eventId][$callbackId]); + + assert($callback instanceof StreamCallback); + + if (empty($this->callbacks[$eventId])) { + unset($this->callbacks[$eventId], $this->streams[(int) $callback->stream]); + } + } + + unset($this->events[$callbackId]); + } + + /** + * @return \UVLoop|resource + */ + public function getHandle() : mixed + { + return $this->handle; + } + + protected function now() : float + { + uv_update_time($this->handle); + + /** @psalm-suppress TooManyArguments */ + return uv_now($this->handle) / 1000; + } + + /** + * {@inheritdoc} + */ + protected function dispatch(bool $blocking) : void + { + /** @psalm-suppress TooManyArguments */ + uv_run($this->handle, $blocking ? \UV::RUN_ONCE : \UV::RUN_NOWAIT); + } + + /** + * {@inheritdoc} + */ + protected function activate(array $callbacks) : void + { + $now = $this->now(); + + foreach ($callbacks as $callback) { + $id = $callback->id; + + if ($callback instanceof StreamCallback) { + assert(is_resource($callback->stream)); + + $streamId = (int) $callback->stream; + + if (isset($this->streams[$streamId])) { + $event = $this->streams[$streamId]; + } elseif (isset($this->events[$id])) { + $event = $this->streams[$streamId] = $this->events[$id]; + } else { + /** @psalm-suppress TooManyArguments */ + $event = $this->streams[$streamId] = uv_poll_init_socket($this->handle, $callback->stream); + } + + $eventId = (int) $event; + $this->events[$id] = $event; + $this->callbacks[$eventId][$id] = $callback; + + $flags = 0; + foreach ($this->callbacks[$eventId] as $w) { + assert($w instanceof StreamCallback); + + $flags |= $w->enabled ? ($this->getStreamCallbackFlags($w)) : 0; + } + uv_poll_start($event, $flags, $this->ioCallback); + } elseif ($callback instanceof TimerCallback) { + if (isset($this->events[$id])) { + $event = $this->events[$id]; + } else { + $event = $this->events[$id] = uv_timer_init($this->handle); + } + + $this->callbacks[(int) $event] = [$callback]; + + uv_timer_start( + $event, + (int) min(max(0, ceil(($callback->expiration - $now) * 1000)), PHP_INT_MAX), + $callback->repeat ? (int) min(max(0, ceil($callback->interval * 1000)), PHP_INT_MAX) : 0, + $this->timerCallback + ); + } elseif ($callback instanceof SignalCallback) { + if (isset($this->events[$id])) { + $event = $this->events[$id]; + } else { + /** @psalm-suppress TooManyArguments */ + $event = $this->events[$id] = uv_signal_init($this->handle); + } + + $this->callbacks[(int) $event] = [$callback]; + + /** @psalm-suppress TooManyArguments */ + uv_signal_start($event, $this->signalCallback, $callback->signal); + } else { + // @codeCoverageIgnoreStart + throw new \Error("Unknown callback type"); + // @codeCoverageIgnoreEnd + } + } + } + + /** + * {@inheritdoc} + */ + protected function deactivate(DriverCallback $callback) : void + { + $id = $callback->id; + + if (!isset($this->events[$id])) { + return; + } + + $event = $this->events[$id]; + + if (!uv_is_active($event)) { + return; + } + + if ($callback instanceof StreamCallback) { + $flags = 0; + foreach ($this->callbacks[(int) $event] as $w) { + assert($w instanceof StreamCallback); + + $flags |= $w->invokable ? ($this->getStreamCallbackFlags($w)) : 0; + } + + if ($flags) { + uv_poll_start($event, $flags, $this->ioCallback); + } else { + uv_poll_stop($event); + } + } elseif ($callback instanceof TimerCallback) { + uv_timer_stop($event); + } elseif ($callback instanceof SignalCallback) { + uv_signal_stop($event); + } else { + // @codeCoverageIgnoreStart + throw new \Error("Unknown callback type"); + // @codeCoverageIgnoreEnd + } + } + + private function getStreamCallbackFlags(StreamCallback $callback) : int + { + if ($callback instanceof StreamWritableCallback) { + return \UV::WRITABLE; + } + + if ($callback instanceof StreamReadableCallback) { + return \UV::READABLE; + } + + throw new \Error('Invalid callback type'); + } +} diff --git a/src/thebigcrafter/Hydrogen/eventLoop/DriverFactory.php b/src/thebigcrafter/Hydrogen/eventLoop/DriverFactory.php new file mode 100644 index 0000000..5a410ae --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/DriverFactory.php @@ -0,0 +1,87 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +namespace thebigcrafter\Hydrogen\eventLoop; + +// @codeCoverageIgnoreStart +use thebigcrafter\Hydrogen\eventLoop\Driver\EvDriver; +use thebigcrafter\Hydrogen\eventLoop\Driver\EventDriver; +use thebigcrafter\Hydrogen\eventLoop\Driver\StreamSelectDriver; +use thebigcrafter\Hydrogen\eventLoop\Driver\TracingDriver; +use thebigcrafter\Hydrogen\eventLoop\Driver\UvDriver; +use function class_exists; +use function getenv; +use function is_subclass_of; +use function sprintf; + +final class DriverFactory +{ + /** + * Creates a new loop instance and chooses the best available driver. + * + * @throws \Error If an invalid class has been specified via REVOLT_LOOP_DRIVER + */ + public function create() : Driver + { + $driver = (function () { + if ($driver = $this->createDriverFromEnv()) { + return $driver; + } + + if (UvDriver::isSupported()) { + return new UvDriver(); + } + + if (EvDriver::isSupported()) { + return new EvDriver(); + } + + if (EventDriver::isSupported()) { + return new EventDriver(); + } + + return new StreamSelectDriver(); + })(); + + if (getenv("REVOLT_DRIVER_DEBUG_TRACE")) { + return new TracingDriver($driver); + } + + return $driver; + } + + private function createDriverFromEnv() : ?Driver + { + $driver = getenv("REVOLT_DRIVER"); + + if (!$driver) { + return null; + } + + if (!class_exists($driver)) { + throw new \Error(sprintf( + "Driver '%s' does not exist.", + $driver + )); + } + + if (!is_subclass_of($driver, Driver::class)) { + throw new \Error(sprintf( + "Driver '%s' is not a subclass of '%s'.", + $driver, + Driver::class + )); + } + + return new $driver(); + } +} +// @codeCoverageIgnoreEnd diff --git a/src/thebigcrafter/Hydrogen/eventLoop/FiberLocal.php b/src/thebigcrafter/Hydrogen/eventLoop/FiberLocal.php new file mode 100644 index 0000000..0a5dec2 --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/FiberLocal.php @@ -0,0 +1,89 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +namespace thebigcrafter\Hydrogen\eventLoop; + +/** + * Fiber local storage. + * + * Each instance stores data separately for each fiber. Usage examples include contextual logging data. + * + * @template T + */ +final class FiberLocal +{ + /** @var \Fiber|null Dummy fiber for {main} */ + private static ?\Fiber $mainFiber = null; + private static ?\WeakMap $localStorage = null; + + public static function clear() : void + { + if (self::$localStorage === null) { + return; + } + + $fiber = \Fiber::getCurrent() ?? self::$mainFiber; + + if ($fiber === null) { + return; + } + + unset(self::$localStorage[$fiber]); + } + + private static function getFiberStorage() : \WeakMap + { + $fiber = \Fiber::getCurrent(); + + if ($fiber === null) { + $fiber = self::$mainFiber ??= new \Fiber(static function () : void { + // dummy fiber for main, as we need some object for the WeakMap + }); + } + + $localStorage = self::$localStorage ??= new \WeakMap(); + return $localStorage[$fiber] ??= new \WeakMap(); + } + + /** + * @param \Closure():T $initializer + */ + public function __construct(private readonly \Closure $initializer) + { + } + + /** + * @param T $value + */ + public function set(mixed $value) : void + { + self::getFiberStorage()[$this] = [$value]; + } + + public function unset() : void + { + unset(self::getFiberStorage()[$this]); + } + + /** + * @return T + */ + public function get() : mixed + { + $fiberStorage = self::getFiberStorage(); + + if (!isset($fiberStorage[$this])) { + $fiberStorage[$this] = [($this->initializer)()]; + } + + return $fiberStorage[$this][0]; + } +} diff --git a/src/thebigcrafter/Hydrogen/eventLoop/Internal/AbstractDriver.php b/src/thebigcrafter/Hydrogen/eventLoop/Internal/AbstractDriver.php new file mode 100644 index 0000000..047a9b9 --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/Internal/AbstractDriver.php @@ -0,0 +1,648 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +namespace thebigcrafter\Hydrogen\eventLoop\Internal; + +use thebigcrafter\Hydrogen\eventLoop\CallbackType; +use thebigcrafter\Hydrogen\eventLoop\Driver; +use thebigcrafter\Hydrogen\eventLoop\FiberLocal; +use thebigcrafter\Hydrogen\eventLoop\InvalidCallbackError; +use thebigcrafter\Hydrogen\eventLoop\Suspension; +use thebigcrafter\Hydrogen\eventLoop\UncaughtThrowable; +use function array_keys; +use function array_map; +use function assert; +use function getenv; +use function sprintf; +use const PHP_VERSION_ID; + +/** + * Event loop driver which implements all basic operations to allow interoperability. + * + * Callbacks (enabled or new callbacks) MUST immediately be marked as enabled, but only be activated (i.e. callbacks can + * be called) right before the next tick. Callbacks MUST NOT be called in the tick they were enabled. + * + * All registered callbacks MUST NOT be called from a file with strict types enabled (`declare(strict_types=1)`). + * + * @internal + */ +abstract class AbstractDriver implements Driver +{ + /** @var string Next callback identifier. */ + private string $nextId = "a"; + + private \Fiber $fiber; + + private \Fiber $callbackFiber; + private \Closure $errorCallback; + + /** @var array */ + private array $callbacks = []; + + /** @var array */ + private array $enableQueue = []; + + /** @var array */ + private array $enableDeferQueue = []; + + /** @var null|\Closure(\Throwable):void */ + private ?\Closure $errorHandler = null; + + /** @var null|\Closure():mixed */ + private ?\Closure $interrupt = null; + + private readonly \Closure $interruptCallback; + private readonly \Closure $queueCallback; + private readonly \Closure $runCallback; + + private readonly \stdClass $internalSuspensionMarker; + + /** @var \SplQueue */ + private readonly \SplQueue $microtaskQueue; + + /** @var \SplQueue */ + private readonly \SplQueue $callbackQueue; + + private bool $idle = false; + private bool $stopped = false; + + private \WeakMap $suspensions; + + public function __construct() + { + if (PHP_VERSION_ID < 80117 || PHP_VERSION_ID >= 80200 && PHP_VERSION_ID < 80204) { + // PHP GC is broken on early 8.1 and 8.2 versions, see https://github.com/php/php-src/issues/10496 + if (!getenv('REVOLT_DRIVER_SUPPRESS_ISSUE_10496')) { + throw new \Error('Your version of PHP is affected by serious garbage collector bugs related to fibers. Please upgrade to a newer version of PHP, i.e. >= 8.1.17 or => 8.2.4'); + } + } + + $this->suspensions = new \WeakMap(); + + $this->internalSuspensionMarker = new \stdClass(); + $this->microtaskQueue = new \SplQueue(); + $this->callbackQueue = new \SplQueue(); + + $this->createLoopFiber(); + $this->createCallbackFiber(); + $this->createErrorCallback(); + + /** @psalm-suppress InvalidArgument */ + $this->interruptCallback = $this->setInterrupt(...); + $this->queueCallback = $this->queue(...); + $this->runCallback = function () { + if ($this->fiber->isTerminated()) { + $this->createLoopFiber(); + } + + return $this->fiber->isStarted() ? $this->fiber->resume() : $this->fiber->start(); + }; + } + + public function run() : void + { + if ($this->fiber->isRunning()) { + throw new \Error("The event loop is already running"); + } + + if (\Fiber::getCurrent()) { + throw new \Error(sprintf("Can't call %s() within a fiber (i.e., outside of {main})", __METHOD__)); + } + + if ($this->fiber->isTerminated()) { + $this->createLoopFiber(); + } + + /** @noinspection PhpUnhandledExceptionInspection */ + $lambda = $this->fiber->isStarted() ? $this->fiber->resume() : $this->fiber->start(); + + if ($lambda) { + $lambda(); + + throw new \Error('Interrupt from event loop must throw an exception: ' . ClosureHelper::getDescription($lambda)); + } + } + + public function stop() : void + { + $this->stopped = true; + } + + public function isRunning() : bool + { + return $this->fiber->isRunning() || $this->fiber->isSuspended(); + } + + public function queue(\Closure $closure, mixed ...$args) : void + { + $this->microtaskQueue->enqueue([$closure, $args]); + } + + public function defer(\Closure $closure) : string + { + $deferCallback = new DeferCallback($this->nextId++, $closure); + + $this->callbacks[$deferCallback->id] = $deferCallback; + $this->enableDeferQueue[$deferCallback->id] = $deferCallback; + + return $deferCallback->id; + } + + public function delay(float $delay, \Closure $closure) : string + { + if ($delay < 0) { + throw new \Error("Delay must be greater than or equal to zero"); + } + + $timerCallback = new TimerCallback($this->nextId++, $delay, $closure, $this->now() + $delay); + + $this->callbacks[$timerCallback->id] = $timerCallback; + $this->enableQueue[$timerCallback->id] = $timerCallback; + + return $timerCallback->id; + } + + public function repeat(float $interval, \Closure $closure) : string + { + if ($interval < 0) { + throw new \Error("Interval must be greater than or equal to zero"); + } + + $timerCallback = new TimerCallback($this->nextId++, $interval, $closure, $this->now() + $interval, true); + + $this->callbacks[$timerCallback->id] = $timerCallback; + $this->enableQueue[$timerCallback->id] = $timerCallback; + + return $timerCallback->id; + } + + public function onReadable(mixed $stream, \Closure $closure) : string + { + $streamCallback = new StreamReadableCallback($this->nextId++, $closure, $stream); + + $this->callbacks[$streamCallback->id] = $streamCallback; + $this->enableQueue[$streamCallback->id] = $streamCallback; + + return $streamCallback->id; + } + + public function onWritable($stream, \Closure $closure) : string + { + $streamCallback = new StreamWritableCallback($this->nextId++, $closure, $stream); + + $this->callbacks[$streamCallback->id] = $streamCallback; + $this->enableQueue[$streamCallback->id] = $streamCallback; + + return $streamCallback->id; + } + + public function onSignal(int $signal, \Closure $closure) : string + { + $signalCallback = new SignalCallback($this->nextId++, $closure, $signal); + + $this->callbacks[$signalCallback->id] = $signalCallback; + $this->enableQueue[$signalCallback->id] = $signalCallback; + + return $signalCallback->id; + } + + public function enable(string $callbackId) : string + { + if (!isset($this->callbacks[$callbackId])) { + throw InvalidCallbackError::invalidIdentifier($callbackId); + } + + $callback = $this->callbacks[$callbackId]; + + if ($callback->enabled) { + return $callbackId; // Callback already enabled. + } + + $callback->enabled = true; + + if ($callback instanceof DeferCallback) { + $this->enableDeferQueue[$callback->id] = $callback; + } elseif ($callback instanceof TimerCallback) { + $callback->expiration = $this->now() + $callback->interval; + $this->enableQueue[$callback->id] = $callback; + } else { + $this->enableQueue[$callback->id] = $callback; + } + + return $callbackId; + } + + public function cancel(string $callbackId) : void + { + $this->disable($callbackId); + unset($this->callbacks[$callbackId]); + } + + public function disable(string $callbackId) : string + { + if (!isset($this->callbacks[$callbackId])) { + return $callbackId; + } + + $callback = $this->callbacks[$callbackId]; + + if (!$callback->enabled) { + return $callbackId; // Callback already disabled. + } + + $callback->enabled = false; + $callback->invokable = false; + $id = $callback->id; + + if ($callback instanceof DeferCallback) { + // Callback was only queued to be enabled. + unset($this->enableDeferQueue[$id]); + } elseif (isset($this->enableQueue[$id])) { + // Callback was only queued to be enabled. + unset($this->enableQueue[$id]); + } else { + $this->deactivate($callback); + } + + return $callbackId; + } + + public function reference(string $callbackId) : string + { + if (!isset($this->callbacks[$callbackId])) { + throw InvalidCallbackError::invalidIdentifier($callbackId); + } + + $this->callbacks[$callbackId]->referenced = true; + + return $callbackId; + } + + public function unreference(string $callbackId) : string + { + if (!isset($this->callbacks[$callbackId])) { + return $callbackId; + } + + $this->callbacks[$callbackId]->referenced = false; + + return $callbackId; + } + + public function getSuspension() : Suspension + { + $fiber = \Fiber::getCurrent(); + + // User callbacks are always executed outside the event loop fiber, so this should always be false. + assert($fiber !== $this->fiber); + + // Use current object in case of {main} + $suspension = ($this->suspensions[$fiber ?? $this] ?? null)?->get(); + if ($suspension) { + return $suspension; + } + + $suspension = new DriverSuspension( + $this->runCallback, + $this->queueCallback, + $this->interruptCallback, + $this->suspensions, + ); + + $this->suspensions[$fiber ?? $this] = \WeakReference::create($suspension); + + return $suspension; + } + + public function setErrorHandler(?\Closure $errorHandler) : void + { + $this->errorHandler = $errorHandler; + } + + public function getErrorHandler() : ?\Closure + { + return $this->errorHandler; + } + + public function __debugInfo() : array + { + // @codeCoverageIgnoreStart + return array_map(fn (DriverCallback $callback) => [ + 'type' => $this->getType($callback->id), + 'enabled' => $callback->enabled, + 'referenced' => $callback->referenced, + ], $this->callbacks); + // @codeCoverageIgnoreEnd + } + + public function getIdentifiers() : array + { + return array_keys($this->callbacks); + } + + public function getType(string $callbackId) : CallbackType + { + $callback = $this->callbacks[$callbackId] ?? throw InvalidCallbackError::invalidIdentifier($callbackId); + + return match ($callback::class) { + DeferCallback::class => CallbackType::Defer, + TimerCallback::class => $callback->repeat ? CallbackType::Repeat : CallbackType::Delay, + StreamReadableCallback::class => CallbackType::Readable, + StreamWritableCallback::class => CallbackType::Writable, + SignalCallback::class => CallbackType::Signal, + }; + } + + public function isEnabled(string $callbackId) : bool + { + $callback = $this->callbacks[$callbackId] ?? throw InvalidCallbackError::invalidIdentifier($callbackId); + + return $callback->enabled; + } + + public function isReferenced(string $callbackId) : bool + { + $callback = $this->callbacks[$callbackId] ?? throw InvalidCallbackError::invalidIdentifier($callbackId); + + return $callback->referenced; + } + + /** + * Activates (enables) all the given callbacks. + */ + abstract protected function activate(array $callbacks) : void; + + /** + * Dispatches any pending read/write, timer, and signal events. + */ + abstract protected function dispatch(bool $blocking) : void; + + /** + * Deactivates (disables) the given callback. + */ + abstract protected function deactivate(DriverCallback $callback) : void; + + final protected function enqueueCallback(DriverCallback $callback) : void + { + $this->callbackQueue->enqueue($callback); + $this->idle = false; + } + + /** + * Invokes the error handler with the given exception. + * + * @param \Throwable $exception The exception thrown from an event callback. + */ + final protected function error(\Closure $closure, \Throwable $exception) : void + { + if ($this->errorHandler === null) { + // Explicitly override the previous interrupt if it exists in this case, hiding the exception is worse + $this->interrupt = static fn () => $exception instanceof UncaughtThrowable + ? throw $exception + : throw UncaughtThrowable::throwingCallback($closure, $exception); + return; + } + + $fiber = new \Fiber($this->errorCallback); + + /** @noinspection PhpUnhandledExceptionInspection */ + $fiber->start($this->errorHandler, $exception); + } + + /** + * Returns the current event loop time in second increments. + * + * Note this value does not necessarily correlate to wall-clock time, rather the value returned is meant to be used + * in relative comparisons to prior values returned by this method (intervals, expiration calculations, etc.). + */ + abstract protected function now() : float; + + private function invokeMicrotasks() : void + { + while (!$this->microtaskQueue->isEmpty()) { + [$callback, $args] = $this->microtaskQueue->dequeue(); + + try { + // Clear $args to allow garbage collection + $callback(...$args, ...($args = [])); + } catch (\Throwable $exception) { + $this->error($callback, $exception); + } finally { + FiberLocal::clear(); + } + + unset($callback, $args); + + if ($this->interrupt) { + /** @noinspection PhpUnhandledExceptionInspection */ + \Fiber::suspend($this->internalSuspensionMarker); + } + } + } + + /** + * @return bool True if no enabled and referenced callbacks remain in the loop. + */ + private function isEmpty() : bool + { + foreach ($this->callbacks as $callback) { + if ($callback->enabled && $callback->referenced) { + return false; + } + } + + return true; + } + + /** + * Executes a single tick of the event loop. + */ + private function tick(bool $previousIdle) : void + { + $this->activate($this->enableQueue); + + foreach ($this->enableQueue as $callback) { + $callback->invokable = true; + } + + $this->enableQueue = []; + + foreach ($this->enableDeferQueue as $callback) { + $callback->invokable = true; + $this->enqueueCallback($callback); + } + + $this->enableDeferQueue = []; + + $blocking = $previousIdle + && !$this->stopped + && !$this->isEmpty(); + + if ($blocking) { + $this->invokeCallbacks(); + + /** @psalm-suppress TypeDoesNotContainType */ + if (!empty($this->enableDeferQueue) || !empty($this->enableQueue)) { + $blocking = false; + } + } + + /** @psalm-suppress RedundantCondition */ + $this->dispatch($blocking); + } + + private function invokeCallbacks() : void + { + while (!$this->microtaskQueue->isEmpty() || !$this->callbackQueue->isEmpty()) { + /** @noinspection PhpUnhandledExceptionInspection */ + $yielded = $this->callbackFiber->isStarted() + ? $this->callbackFiber->resume() + : $this->callbackFiber->start(); + + if ($yielded !== $this->internalSuspensionMarker) { + $this->createCallbackFiber(); + } + + if ($this->interrupt) { + $this->invokeInterrupt(); + } + } + } + + /** + * @param \Closure():mixed $interrupt + */ + private function setInterrupt(\Closure $interrupt) : void + { + assert($this->interrupt === null); + + $this->interrupt = $interrupt; + } + + private function invokeInterrupt() : void + { + assert($this->interrupt !== null); + + $interrupt = $this->interrupt; + $this->interrupt = null; + + /** @noinspection PhpUnhandledExceptionInspection */ + \Fiber::suspend($interrupt); + } + + private function createLoopFiber() : void + { + $this->fiber = new \Fiber(function () : void { + $this->stopped = false; + + // Invoke microtasks if we have some + $this->invokeCallbacks(); + + while (!$this->stopped) { + if ($this->interrupt) { + $this->invokeInterrupt(); + } + + if ($this->isEmpty()) { + return; + } + + $previousIdle = $this->idle; + $this->idle = true; + + $this->tick($previousIdle); + $this->invokeCallbacks(); + } + }); + } + + private function createCallbackFiber() : void + { + $this->callbackFiber = new \Fiber(function () : void { + do { + $this->invokeMicrotasks(); + + while (!$this->callbackQueue->isEmpty()) { + /** @var DriverCallback $callback */ + $callback = $this->callbackQueue->dequeue(); + + if (!isset($this->callbacks[$callback->id]) || !$callback->invokable) { + unset($callback); + + continue; + } + + if ($callback instanceof DeferCallback) { + $this->cancel($callback->id); + } elseif ($callback instanceof TimerCallback) { + if (!$callback->repeat) { + $this->cancel($callback->id); + } else { + // Disable and re-enable, so it's not executed repeatedly in the same tick + // See https://github.com/amphp/amp/issues/131 + $this->disable($callback->id); + $this->enable($callback->id); + } + } + + try { + $result = match (true) { + $callback instanceof StreamCallback => ($callback->closure)( + $callback->id, + $callback->stream + ), + $callback instanceof SignalCallback => ($callback->closure)( + $callback->id, + $callback->signal + ), + default => ($callback->closure)($callback->id), + }; + + if ($result !== null) { + throw InvalidCallbackError::nonNullReturn($callback->id, $callback->closure); + } + } catch (\Throwable $exception) { + $this->error($callback->closure, $exception); + } finally { + FiberLocal::clear(); + } + + unset($callback); + + if ($this->interrupt) { + /** @noinspection PhpUnhandledExceptionInspection */ + \Fiber::suspend($this->internalSuspensionMarker); + } + + $this->invokeMicrotasks(); + } + + /** @noinspection PhpUnhandledExceptionInspection */ + \Fiber::suspend($this->internalSuspensionMarker); + } while (true); + }); + } + + private function createErrorCallback() : void + { + $this->errorCallback = function (\Closure $errorHandler, \Throwable $exception) : void { + try { + $errorHandler($exception); + } catch (\Throwable $exception) { + $this->setInterrupt( + static fn () => $exception instanceof UncaughtThrowable + ? throw $exception + : throw UncaughtThrowable::throwingErrorHandler($errorHandler, $exception) + ); + } + }; + } +} diff --git a/src/thebigcrafter/Hydrogen/eventLoop/Internal/ClosureHelper.php b/src/thebigcrafter/Hydrogen/eventLoop/Internal/ClosureHelper.php new file mode 100644 index 0000000..5c9bfd9 --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/Internal/ClosureHelper.php @@ -0,0 +1,37 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +namespace thebigcrafter\Hydrogen\eventLoop\Internal; + +/** @internal */ +final class ClosureHelper +{ + public static function getDescription(\Closure $closure) : string + { + try { + $reflection = new \ReflectionFunction($closure); + + $description = $reflection->name; + + if ($scopeClass = $reflection->getClosureScopeClass()) { + $description = $scopeClass->name . '::' . $description; + } + + if ($reflection->getFileName() && $reflection->getStartLine()) { + $description .= " defined in " . $reflection->getFileName() . ':' . $reflection->getStartLine(); + } + + return $description; + } catch (\ReflectionException) { + return '???'; + } + } +} diff --git a/src/thebigcrafter/Hydrogen/eventLoop/Internal/DeferCallback.php b/src/thebigcrafter/Hydrogen/eventLoop/Internal/DeferCallback.php new file mode 100644 index 0000000..ef0fe17 --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/Internal/DeferCallback.php @@ -0,0 +1,17 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +namespace thebigcrafter\Hydrogen\eventLoop\Internal; + +/** @internal */ +final class DeferCallback extends DriverCallback +{ +} diff --git a/src/thebigcrafter/Hydrogen/eventLoop/Internal/DriverCallback.php b/src/thebigcrafter/Hydrogen/eventLoop/Internal/DriverCallback.php new file mode 100644 index 0000000..d1309e0 --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/Internal/DriverCallback.php @@ -0,0 +1,40 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +namespace thebigcrafter\Hydrogen\eventLoop\Internal; + +/** + * @internal + */ +abstract class DriverCallback +{ + public bool $invokable = false; + + public bool $enabled = true; + + public bool $referenced = true; + + public function __construct( + public readonly string $id, + public readonly \Closure $closure + ) { + } + + public function __get(string $property) : never + { + throw new \Error("Unknown property '{$property}'"); + } + + public function __set(string $property, mixed $value) : never + { + throw new \Error("Unknown property '{$property}'"); + } +} diff --git a/src/thebigcrafter/Hydrogen/eventLoop/Internal/DriverSuspension.php b/src/thebigcrafter/Hydrogen/eventLoop/Internal/DriverSuspension.php new file mode 100644 index 0000000..d440ef2 --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/Internal/DriverSuspension.php @@ -0,0 +1,179 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +namespace thebigcrafter\Hydrogen\eventLoop\Internal; + +use thebigcrafter\Hydrogen\eventLoop\Suspension; +use function array_keys; +use function array_map; +use function assert; +use function gc_collect_cycles; +use function implode; +use const DEBUG_BACKTRACE_IGNORE_ARGS; + +/** + * @internal + * + * @template T + * @implements Suspension + */ +final class DriverSuspension implements Suspension +{ + private ?\Fiber $suspendedFiber = null; + + /** @var \WeakReference<\Fiber>|null */ + private readonly ?\WeakReference $fiberRef; + + private ?\FiberError $fiberError = null; + + private bool $pending = false; + + public function __construct( + private readonly \Closure $run, + private readonly \Closure $queue, + private readonly \Closure $interrupt, + private readonly \WeakMap $suspensions, + ) { + $fiber = \Fiber::getCurrent(); + + $this->fiberRef = $fiber ? \WeakReference::create($fiber) : null; + } + + public function resume(mixed $value = null) : void + { + if (!$this->pending) { + throw $this->fiberError ?? new \Error('Must call suspend() before calling resume()'); + } + + $this->pending = false; + + /** @var \Fiber|null $fiber */ + $fiber = $this->fiberRef?->get(); + + if ($fiber) { + ($this->queue)(static function () use ($fiber, $value) : void { + // The fiber may be destroyed with suspension as part of the GC cycle collector. + if (!$fiber->isTerminated()) { + $fiber->resume($value); + } + }); + } else { + // Suspend event loop fiber to {main}. + ($this->interrupt)(static fn () => $value); + } + } + + public function suspend() : mixed + { + if ($this->pending) { + throw new \Error('Must call resume() or throw() before calling suspend() again'); + } + + $fiber = $this->fiberRef?->get(); + + if ($fiber !== \Fiber::getCurrent()) { + throw new \Error('Must not call suspend() from another fiber'); + } + + $this->pending = true; + + // Awaiting from within a fiber. + if ($fiber) { + $this->suspendedFiber = $fiber; + + try { + $value = \Fiber::suspend(); + $this->suspendedFiber = null; + } catch (\FiberError $exception) { + $this->pending = false; + $this->suspendedFiber = null; + $this->fiberError = $exception; + + throw $exception; + } + + // Setting $this->suspendedFiber = null in finally will set the fiber to null if a fiber is destroyed + // as part of a cycle collection, causing an error if the suspension is subsequently resumed. + + return $value; + } + + // Awaiting from {main}. + $result = ($this->run)(); + + /** @psalm-suppress RedundantCondition $this->pending should be changed when resumed. */ + if ($this->pending) { + $this->pending = false; + $result && $result(); // Unwrap any uncaught exceptions from the event loop + + gc_collect_cycles(); // Collect any circular references before dumping pending suspensions. + + $info = ''; + foreach ($this->suspensions as $suspensionRef) { + if ($suspension = $suspensionRef->get()) { + assert($suspension instanceof self); + $fiber = $suspension->fiberRef?->get(); + if ($fiber === null) { + continue; + } + + $reflectionFiber = new \ReflectionFiber($fiber); + $info .= "\n\n" . $this->formatStacktrace($reflectionFiber->getTrace(DEBUG_BACKTRACE_IGNORE_ARGS)); + } + } + + throw new \Error('Event loop terminated without resuming the current suspension (the cause is either a fiber deadlock, or an incorrectly unreferenced/canceled watcher):' . $info); + } + + return $result(); + } + + public function throw(\Throwable $throwable) : void + { + if (!$this->pending) { + throw $this->fiberError ?? new \Error('Must call suspend() before calling throw()'); + } + + $this->pending = false; + + /** @var \Fiber|null $fiber */ + $fiber = $this->fiberRef?->get(); + + if ($fiber) { + ($this->queue)(static function () use ($fiber, $throwable) : void { + // The fiber may be destroyed with suspension as part of the GC cycle collector. + if (!$fiber->isTerminated()) { + $fiber->throw($throwable); + } + }); + } else { + // Suspend event loop fiber to {main}. + ($this->interrupt)(static fn () => throw $throwable); + } + } + + private function formatStacktrace(array $trace) : string + { + return implode("\n", array_map(static function ($e, $i) { + $line = "#{$i} "; + + if (isset($e["file"])) { + $line .= "{$e['file']}:{$e['line']} "; + } + + if (isset($e["class"], $e["type"])) { + $line .= $e["class"] . $e["type"]; + } + + return $line . $e["function"] . "()"; + }, $trace, array_keys($trace))); + } +} diff --git a/src/thebigcrafter/Hydrogen/eventLoop/Internal/SignalCallback.php b/src/thebigcrafter/Hydrogen/eventLoop/Internal/SignalCallback.php new file mode 100644 index 0000000..7a6dc26 --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/Internal/SignalCallback.php @@ -0,0 +1,24 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +namespace thebigcrafter\Hydrogen\eventLoop\Internal; + +/** @internal */ +final class SignalCallback extends DriverCallback +{ + public function __construct( + string $id, + \Closure $closure, + public readonly int $signal + ) { + parent::__construct($id, $closure); + } +} diff --git a/src/thebigcrafter/Hydrogen/eventLoop/Internal/StreamCallback.php b/src/thebigcrafter/Hydrogen/eventLoop/Internal/StreamCallback.php new file mode 100644 index 0000000..39bc10d --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/Internal/StreamCallback.php @@ -0,0 +1,27 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +namespace thebigcrafter\Hydrogen\eventLoop\Internal; + +/** @internal */ +abstract class StreamCallback extends DriverCallback +{ + /** + * @param resource $stream + */ + public function __construct( + string $id, + \Closure $closure, + public readonly mixed $stream + ) { + parent::__construct($id, $closure); + } +} diff --git a/src/thebigcrafter/Hydrogen/eventLoop/Internal/StreamReadableCallback.php b/src/thebigcrafter/Hydrogen/eventLoop/Internal/StreamReadableCallback.php new file mode 100644 index 0000000..1c0a41f --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/Internal/StreamReadableCallback.php @@ -0,0 +1,17 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +namespace thebigcrafter\Hydrogen\eventLoop\Internal; + +/** @internal */ +final class StreamReadableCallback extends StreamCallback +{ +} diff --git a/src/thebigcrafter/Hydrogen/eventLoop/Internal/StreamWritableCallback.php b/src/thebigcrafter/Hydrogen/eventLoop/Internal/StreamWritableCallback.php new file mode 100644 index 0000000..815146f --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/Internal/StreamWritableCallback.php @@ -0,0 +1,17 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +namespace thebigcrafter\Hydrogen\eventLoop\Internal; + +/** @internal */ +final class StreamWritableCallback extends StreamCallback +{ +} diff --git a/src/thebigcrafter/Hydrogen/eventLoop/Internal/TimerCallback.php b/src/thebigcrafter/Hydrogen/eventLoop/Internal/TimerCallback.php new file mode 100644 index 0000000..9c0a8ae --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/Internal/TimerCallback.php @@ -0,0 +1,26 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +namespace thebigcrafter\Hydrogen\eventLoop\Internal; + +/** @internal */ +final class TimerCallback extends DriverCallback +{ + public function __construct( + string $id, + public readonly float $interval, + \Closure $callback, + public float $expiration, + public readonly bool $repeat = false + ) { + parent::__construct($id, $callback); + } +} diff --git a/src/thebigcrafter/Hydrogen/eventLoop/Internal/TimerQueue.php b/src/thebigcrafter/Hydrogen/eventLoop/Internal/TimerQueue.php new file mode 100644 index 0000000..491d95d --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/Internal/TimerQueue.php @@ -0,0 +1,166 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +namespace thebigcrafter\Hydrogen\eventLoop\Internal; + +use function assert; +use function count; + +/** + * Uses a binary tree stored in an array to implement a heap. + * + * @internal + */ +final class TimerQueue +{ + /** @var array */ + private array $callbacks = []; + + /** @var array */ + private array $pointers = []; + + /** + * Inserts the callback into the queue. + * + * Time complexity: O(log(n)). + */ + public function insert(TimerCallback $callback) : void + { + assert(!isset($this->pointers[$callback->id])); + + $node = count($this->callbacks); + $this->callbacks[$node] = $callback; + $this->pointers[$callback->id] = $node; + + $this->heapifyUp($node); + } + + /** + * Removes the given callback from the queue. + * + * Time complexity: O(log(n)). + */ + public function remove(TimerCallback $callback) : void + { + $id = $callback->id; + + if (!isset($this->pointers[$id])) { + return; + } + + $this->removeAndRebuild($this->pointers[$id]); + } + + /** + * Deletes and returns the callback on top of the heap if it has expired, otherwise null is returned. + * + * Time complexity: O(log(n)). + * + * @param float $now Current event loop time. + * + * @return TimerCallback|null Expired callback at the top of the heap or null if the callback has not expired. + */ + public function extract(float $now) : ?TimerCallback + { + if (!$this->callbacks) { + return null; + } + + $callback = $this->callbacks[0]; + if ($callback->expiration > $now) { + return null; + } + + $this->removeAndRebuild(0); + + return $callback; + } + + /** + * Returns the expiration time value at the top of the heap. + * + * Time complexity: O(1). + * + * @return float|null Expiration time of the callback at the top of the heap or null if the heap is empty. + */ + public function peek() : ?float + { + return isset($this->callbacks[0]) ? $this->callbacks[0]->expiration : null; + } + + /** + * @param int $node Rebuild the data array from the given node upward. + */ + private function heapifyUp(int $node) : void + { + $entry = $this->callbacks[$node]; + while ($node !== 0 && $entry->expiration < $this->callbacks[$parent = ($node - 1) >> 1]->expiration) { + $this->swap($node, $parent); + $node = $parent; + } + } + + /** + * @param int $node Rebuild the data array from the given node downward. + */ + private function heapifyDown(int $node) : void + { + $length = count($this->callbacks); + while (($child = ($node << 1) + 1) < $length) { + if ($this->callbacks[$child]->expiration < $this->callbacks[$node]->expiration + && ($child + 1 >= $length || $this->callbacks[$child]->expiration < $this->callbacks[$child + 1]->expiration) + ) { + // Left child is less than parent and right child. + $swap = $child; + } elseif ($child + 1 < $length && $this->callbacks[$child + 1]->expiration < $this->callbacks[$node]->expiration) { + // Right child is less than parent and left child. + $swap = $child + 1; + } else { // Left and right child are greater than parent. + break; + } + + $this->swap($node, $swap); + $node = $swap; + } + } + + private function swap(int $left, int $right) : void + { + $temp = $this->callbacks[$left]; + + $this->callbacks[$left] = $this->callbacks[$right]; + $this->pointers[$this->callbacks[$right]->id] = $left; + + $this->callbacks[$right] = $temp; + $this->pointers[$temp->id] = $right; + } + + /** + * @param int $node Remove the given node and then rebuild the data array. + */ + private function removeAndRebuild(int $node) : void + { + $length = count($this->callbacks) - 1; + $id = $this->callbacks[$node]->id; + $left = $this->callbacks[$node] = $this->callbacks[$length]; + $this->pointers[$left->id] = $node; + unset($this->callbacks[$length], $this->pointers[$id]); + + if ($node < $length) { // don't need to do anything if we removed the last element + $parent = ($node - 1) >> 1; + if ($parent >= 0 && $this->callbacks[$node]->expiration < $this->callbacks[$parent]->expiration) { + $this->heapifyUp($node); + } else { + $this->heapifyDown($node); + } + } + } +} diff --git a/src/thebigcrafter/Hydrogen/eventLoop/InvalidCallbackError.php b/src/thebigcrafter/Hydrogen/eventLoop/InvalidCallbackError.php new file mode 100644 index 0000000..f31b388 --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/InvalidCallbackError.php @@ -0,0 +1,82 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +namespace thebigcrafter\Hydrogen\eventLoop; + +use thebigcrafter\Hydrogen\eventLoop\Internal\ClosureHelper; + +final class InvalidCallbackError extends \Error +{ + public const E_NONNULL_RETURN = 1; + public const E_INVALID_IDENTIFIER = 2; + + /** + * MUST be thrown if any callback returns a non-null value. + */ + public static function nonNullReturn(string $callbackId, \Closure $closure) : self + { + return new self( + $callbackId, + self::E_NONNULL_RETURN, + 'Non-null return value received from callback ' . ClosureHelper::getDescription($closure) + ); + } + + /** + * MUST be thrown if any operation (except disable() and cancel()) is attempted with an invalid callback identifier. + * + * An invalid callback identifier is any identifier that is not yet emitted by the driver or cancelled by the user. + */ + public static function invalidIdentifier(string $callbackId) : self + { + return new self($callbackId, self::E_INVALID_IDENTIFIER, 'Invalid callback identifier ' . $callbackId); + } + + private readonly string $rawMessage; + + private readonly string $callbackId; + + /** @var array */ + private array $info = []; + + /** + * @param string $callbackId The callback identifier. + * @param string $message The exception message. + */ + private function __construct(string $callbackId, int $code, string $message) + { + parent::__construct($message, $code); + + $this->callbackId = $callbackId; + $this->rawMessage = $message; + } + + /** + * @return string The callback identifier. + */ + public function getCallbackId() : string + { + return $this->callbackId; + } + + public function addInfo(string $key, string $message) : void + { + $this->info[$key] = $message; + + $info = ''; + + foreach ($this->info as $infoKey => $infoMessage) { + $info .= "\r\n\r\n" . $infoKey . ': ' . $infoMessage; + } + + $this->message = $this->rawMessage . $info; + } +} diff --git a/src/thebigcrafter/Hydrogen/eventLoop/Suspension.php b/src/thebigcrafter/Hydrogen/eventLoop/Suspension.php new file mode 100644 index 0000000..0d33048 --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/Suspension.php @@ -0,0 +1,50 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +namespace thebigcrafter\Hydrogen\eventLoop; + +/** + * Should be used to run and suspend the event loop instead of directly interacting with fibers. + * + * **Example** + * + * ```php + * $suspension = EventLoop::getSuspension(); + * + * $promise->then( + * fn (mixed $value) => $suspension->resume($value), + * fn (Throwable $error) => $suspension->throw($error) + * ); + * + * $suspension->suspend(); + * ``` + * + * @template T + */ +interface Suspension +{ + /** + * @param T $value The value to return from the call to {@see suspend()}. + */ + public function resume(mixed $value = null) : void; + + /** + * Returns the value provided to {@see resume()} or throws the exception provided to {@see throw()}. + * + * @return T + */ + public function suspend() : mixed; + + /** + * Throws the given exception from the call to {@see suspend()}. + */ + public function throw(\Throwable $throwable) : void; +} diff --git a/src/thebigcrafter/Hydrogen/eventLoop/UncaughtThrowable.php b/src/thebigcrafter/Hydrogen/eventLoop/UncaughtThrowable.php new file mode 100644 index 0000000..f31f15a --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/UncaughtThrowable.php @@ -0,0 +1,44 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +namespace thebigcrafter\Hydrogen\eventLoop; + +use thebigcrafter\Hydrogen\eventLoop\Internal\ClosureHelper; +use function get_class; +use function sprintf; +use function str_replace; + +final class UncaughtThrowable extends \Error +{ + public static function throwingCallback(\Closure $closure, \Throwable $previous) : self + { + return new self( + "Uncaught %s thrown in event loop callback %s; use Revolt\EventLoop::setErrorHandler() to gracefully handle such exceptions%s", + $closure, + $previous + ); + } + + public static function throwingErrorHandler(\Closure $closure, \Throwable $previous) : self + { + return new self("Uncaught %s thrown in event loop error handler %s%s", $closure, $previous); + } + + private function __construct(string $message, \Closure $closure, \Throwable $previous) + { + parent::__construct(sprintf( + $message, + str_replace("\0", '@', get_class($previous)), // replace NUL-byte in anonymous class name + ClosureHelper::getDescription($closure), + $previous->getMessage() !== '' ? ': ' . $previous->getMessage() : '' + ), 0, $previous); + } +} diff --git a/src/thebigcrafter/Hydrogen/eventLoop/UnsupportedFeatureException.php b/src/thebigcrafter/Hydrogen/eventLoop/UnsupportedFeatureException.php new file mode 100644 index 0000000..58d2e7d --- /dev/null +++ b/src/thebigcrafter/Hydrogen/eventLoop/UnsupportedFeatureException.php @@ -0,0 +1,21 @@ + + * This source file is subject to the Apache-2.0 license that is bundled + * with this source code in the file LICENSE. + */ + +declare(strict_types=1); + +namespace thebigcrafter\Hydrogen\eventLoop; + +/** + * MUST be thrown if a feature is not supported by the system. + * + * This might happen if ext-pcntl is missing and the loop driver doesn't support another way to dispatch signals. + */ +final class UnsupportedFeatureException extends \Exception +{ +}