diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..12bafa4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore +.travis.sh export-ignore +composer.lock export-ignore +tests/ export-ignore +.hhconfig export-ignore +hhast-lint.json export-ignore +*.hack linguist-language=Hack diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..841a241 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +composer.phar +composer.lock +vendor/ +docker/ +docker-compose.yml +.vscode +*.hhast.parser-cache diff --git a/.hhconfig b/.hhconfig new file mode 100644 index 0000000..37bbd9c --- /dev/null +++ b/.hhconfig @@ -0,0 +1,13 @@ +assume_php = false +enable_experimental_tc_features = no_fallback_in_namespaces +ignored_paths = [ "vendor/.+/tests/.+", "vendor/bin/.*"] +disallow_assign_by_ref = false +unsafe_rx = false +disable_static_local_variables = true +disable_instanceof_refinement = true +disallow_array_literal = false +disable_lval_as_an_expression = true +new_inference_lambda = true +error_php_lambdas = true +allowed_decl_fixme_codes=2053,4045,4047 +allowed_fixme_codes_strict=2011,2049,2050,2053,2083,3004,3084,4027,4045,4047,4053,4104,4107,4106,4108,4110,4119,4128,4135,4188,4223,4240,4248,4323,4200 diff --git a/.travis.sh b/.travis.sh new file mode 100755 index 0000000..56673b9 --- /dev/null +++ b/.travis.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -ex +apt update -y +DEBIAN_FRONTEND=noninteractive apt install -y php-cli zip unzip +hhvm --version +php --version + +( + cd $(mktemp -d) + curl https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer +) + +if (hhvm --version | grep -q -- -dev); then + # Doesn't exist in master, but keep it here so that we can test release + # branches on nightlies too + rm -f composer.lock +fi +composer install +hh_client +./vendor/bin/hacktest.hack tests/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a003dfd --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +sudo: required +language: generic +services: +- docker +env: + matrix: + - HHVM_VERSION=4.62.0 + - HHVM_VERSION=4.64.0 + - HHVM_VERSION=4.65.0 + - HHVM_VERSION=latest +install: +- docker pull hhvm/hhvm-proxygen:$HHVM_VERSION +script: +- docker run --rm -w /var/source -v $(pwd):/var/source hhvm/hhvm:$HHVM_VERSION ./.travis.sh +notifications: + slack: + secure: iOveDi8HmYUL5yiAXSnRBmUoIJOjxPq/tAgmAHYnScZAqEyh9ST8zoixscmenRMdU26X0XZaU6o1bKti/fvA/jPgkt39gxjZl7Z87HFncXU1bTFNC7+v8j4Ri2WmBoFQAnWun0pXOEPt3YZ5u3vRs0ZpRrBCtyRFzt8S+obw7ZzUAz6egmShYzieUiohCy3jzhPxfZeBPfdnrdh7Y1+2h7JC/m60gWn9CZE0Gn62TeJecz1td7xwUgcZQ5ZdXzSeA6jKrSYIrNa3Mn2w+PF8rbiOGrNDvbCwGBaw+P2BTYZ0GQrUv1I8jXETsWTs6R1qZTgv0ZlcsXP2icx1lE3rBeIpcyjj5CpK/fnlFGCV4Pi0RA8OPBvSYJ8KIUJ8f6jOdXLpz8iko1WQCnEEky2oLZLBFaK4uRV7t/fzJ87hINZ/V0NQV/m/6oeerpaM816DBX937TPoonkwjqHvzXb6HvQ/gpq53XyhFi/77rU2fl/KHuCEB9jL6WxBBbrINttlc+jlc7hpG72JUfcU0WlzQg2+KPHa30Lu6SRnSk6bk/IHrSTgeirIXi1kykA3e5UsKr+H+R/cQV6SibOo1sN2TaCebBBJW//kHYFZXqzX5l2kRSPXHwoFQpDhElOKMbbNuBySq2vXFexoadl1q6GOXTh1rhwf36HfrRdAnowwVTo= diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d55015b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017-2020 Yuuki Takezawa + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f1859c3 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# event-dispatcher + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..178c5a0 --- /dev/null +++ b/composer.json @@ -0,0 +1,45 @@ +{ + "name": "nazg/event-dispatcher", + "description": "Event Managiment for Hack", + "keywords": [ + "hhvm", + "hack", + "event" + ], + "license": "MIT", + "authors": [ + { + "name": "Yuuki Takezawa", + "email": "yuuki.takezawa@comnect.jp.net" + } + ], + "require": { + "hhvm": "^4.62", + "hhvm/hsl": "^4.0", + "hhvm/hsl-experimental": "^4.50", + "hhvm/hhvm-autoload": "^3.0" + }, + "require-dev": { + "facebook/fbexpect": "^2.6.1", + "hhvm/hhast": "^4.0", + "hhvm/hacktest": "^2.0" + }, + "autoload": { + "psr-4": { + "Nazg\\EventDispatcher\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "NazgTest\\": "tests/" + } + }, + "scripts": { + "tests": [ + "hhvm ./vendor/bin/hacktest.hack tests/" + ], + "register": [ + "hhvm ./vendor/bin/hh-autoload.hack" + ] + } +} diff --git a/hh_autoload.json b/hh_autoload.json new file mode 100644 index 0000000..08c07f5 --- /dev/null +++ b/hh_autoload.json @@ -0,0 +1,10 @@ +{ + "roots": [ + "src/" + ], + "devRoots": [ + "tests/" + ], + "devFailureHandler": null, + "parser": "ext-factparse" +} diff --git a/hhast-lint.json b/hhast-lint.json new file mode 100644 index 0000000..feac571 --- /dev/null +++ b/hhast-lint.json @@ -0,0 +1,4 @@ +{ + "roots": ["src/", "tests/"], + "builtinLinters": "all" +} diff --git a/src/Event.hack b/src/Event.hack new file mode 100644 index 0000000..011c8d5 --- /dev/null +++ b/src/Event.hack @@ -0,0 +1,15 @@ +namespace Nazg\EventDispatcher; + +class Event implements StoppableEventInterface { + + private bool $propagationStopped = false; + + <<__Rx>> + public function isPropagationStopped(): bool { + return $this->propagationStopped; + } + + public function stopPropagation(): void { + $this->propagationStopped = true; + } +} diff --git a/src/EventDispatcher.hack b/src/EventDispatcher.hack new file mode 100644 index 0000000..8a5c13a --- /dev/null +++ b/src/EventDispatcher.hack @@ -0,0 +1,92 @@ +namespace Nazg\EventDispatcher; + +use namespace HH\Lib\{Dict, C}; +use function array_key_exists; +use function array_filter; + +class EventDispatcher { + + private dict>> $listeners = dict[]; + private dict> $sorted = dict[]; + + public function dispatch(Event $event, string $eventName): Event { + $listeners = $this->getListeners($eventName); + if ($listeners) { + $this->callListeners($listeners, $eventName, $event); + } + return $event; + } + + <<__Rx>> + public function hasListeners( + ?string $eventName = null + ): bool { + if ($eventName is nonnull) { + return !array_key_exists($eventName, $this->listeners); + } + if(C\count($this->listeners)) { + return true; + } + return false; + } + + public function addListener( + string $eventName, + ListenerInterface $listener, + int $priority = 0 + ): void { + $this->listeners[$eventName][$priority][] = $listener; + } + + protected function callListeners( + dict $listeners, + string $eventName, + Event $event + ): void { + foreach ($listeners as $listener) { + if ($event->isPropagationStopped()) { + break; + } + $listener->handle($event, $eventName, $this); + } + } + + private function sortListeners( + string $eventName + ): void { + Dict\sort_by_key($this->listeners[$eventName]); + $this->sorted[$eventName] = dict[]; + foreach ($this->listeners[$eventName] as $listeners) { + foreach ($listeners as $k => $listener) { + $this->sorted[$eventName][] = $listener; + } + } + } + + public function getListeners( + ?string $eventName = null + ): dict { + if ($eventName is nonnull) { + if (!C\count($this->listeners[$eventName])) { + return dict[]; + } + + if (!array_key_exists($eventName, $this->sorted)) { + $this->sortListeners($eventName); + } + return $this->sorted[$eventName]; + } + foreach ($this->listeners as $eventName => $eventListeners) { + if (!array_key_exists($eventName, $this->sorted)) { + $this->sortListeners($eventName); + } + } + $sorted = array_filter($this->sorted); + $d = dict[]; + foreach ($sorted as $key => $value) { + $d[$key] = $value; + } + /* HH_FIXME[4110] */ + return $d; + } +} diff --git a/src/ListenerInterface.hack b/src/ListenerInterface.hack new file mode 100644 index 0000000..7de6d39 --- /dev/null +++ b/src/ListenerInterface.hack @@ -0,0 +1,10 @@ +namespace Nazg\EventDispatcher; + +interface ListenerInterface { + + public function handle( + Event $event, + string $eventName, + EventDispatcher $dispathcher + ): void; +} diff --git a/src/StoppableEventInterface.hack b/src/StoppableEventInterface.hack new file mode 100644 index 0000000..89f237f --- /dev/null +++ b/src/StoppableEventInterface.hack @@ -0,0 +1,12 @@ +namespace Nazg\EventDispatcher; + +interface StoppableEventInterface { + + /** + * Is propagation stopped? + * + * This will typically only be used by the Dispatcher to determine if the + * previous listener halted propagation. + */ + public function isPropagationStopped() : bool; +} diff --git a/tests/EventDispatcherTest.hack b/tests/EventDispatcherTest.hack new file mode 100644 index 0000000..a459ecd --- /dev/null +++ b/tests/EventDispatcherTest.hack @@ -0,0 +1,21 @@ +use type Nazg\EventDispatcher\{Event, EventDispatcher, ListenerInterface}; +use type Facebook\HackTest\HackTest; +use function Facebook\FBExpect\expect; + +final class EventDispatcherTest extends HackTest { + + public async function testShouldBeEmptyDict(): Awaitable { + $dispatcher = new EventDispatcher(); + expect($dispatcher->getListeners())->toBeSame(dict[]); + } +} + +class MockListener implements ListenerInterface { + public function handle( + Event $event, + string $eventName, + EventDispatcher $dispathcher + ): void { + + } +}