-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
238 additions
and
154 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1 @@ | ||
# event-dispatcher | ||
|
||
# Nazg\Dispatcher |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
namespace Nazg\Dispatcher; | ||
|
||
use namespace HH\Lib\{Dict, C}; | ||
use function array_key_exists; | ||
|
||
type DispatchToken = string; | ||
|
||
class Dispatcher<TPayload> { | ||
|
||
private string $prefix = 'ID_'; | ||
private ?TPayload $pendingPayload = null; | ||
|
||
public function __construct( | ||
private dict<DispatchToken, (function(TPayload):void)> $callbacks = dict[], | ||
private bool $isDispatching = false, | ||
private dict<DispatchToken, bool> $isHandled = dict[], | ||
private dict<DispatchToken, bool> $isPending = dict[], | ||
private int $lastID = 1, | ||
) { } | ||
|
||
public function register( | ||
(function(TPayload):void) $callback | ||
): DispatchToken { | ||
$this->lastID = $this->lastID + 1; | ||
$id = $this->prefix . $this->lastID; | ||
$this->callbacks[$id] = $callback; | ||
return $id; | ||
} | ||
|
||
public function unregister(DispatchToken $id): void { | ||
invariant( | ||
array_key_exists($id, $this->callbacks), | ||
'Dispatcher.unregister(...): `%s` does not map to a registered callback.', | ||
$id | ||
); | ||
$this->callbacks = Dict\filter_with_key( | ||
$this->callbacks, | ||
($k, $_) ==> $k !== $id | ||
); | ||
} | ||
|
||
public function waitFor( | ||
vec<DispatchToken> $ids | ||
): void { | ||
invariant( | ||
$this->isDispatching, | ||
'waitFor(...): Must be invoked while dispatching.' | ||
); | ||
for ($i = 0; $i < C\count($ids); $i++) { | ||
$id = $ids[$i]; | ||
if($this->isPending[$id]) { | ||
invariant( | ||
$this->isHandled[$id], | ||
'waitFor(...): Circular dependency detected while waiting for `%s`.', | ||
$id | ||
); | ||
continue; | ||
} | ||
invariant( | ||
$this->callbacks[$id], | ||
'waitFor(...): `%s` does not map to a registered callback.', | ||
$id | ||
); | ||
$this->invokeCallback($id); | ||
} | ||
} | ||
|
||
public function dispatch(TPayload $payload): void { | ||
invariant( | ||
!$this->isDispatching, | ||
'dispatch(...): Cannot dispatch in the middle of a dispatch.' | ||
); | ||
$this->startDispatching($payload); | ||
try { | ||
foreach ($this->callbacks as $key => $value) { | ||
if($this->isPending[$key]) { | ||
continue; | ||
} | ||
$this->invokeCallback($key); | ||
} | ||
} finally { | ||
$this->stopDispatching(); | ||
} | ||
} | ||
|
||
public function isDispatching(): bool { | ||
return $this->isDispatching; | ||
} | ||
|
||
private function invokeCallback(DispatchToken $id): void { | ||
$this->isPending[$id] = true; | ||
if(array_key_exists($id, $this->callbacks)) { | ||
if($this->pendingPayload is nonnull) { | ||
$this->callbacks[$id]($this->pendingPayload); | ||
} | ||
} | ||
$this->isHandled[$id] = true; | ||
} | ||
|
||
private function startDispatching( | ||
TPayload $payload | ||
): void { | ||
Dict\map_with_key($this->callbacks, ($k, $_) ==> { | ||
$this->isPending[$k] = false; | ||
$this->isHandled[$k] = false; | ||
}); | ||
$this->pendingPayload = $payload; | ||
$this->isDispatching = true; | ||
} | ||
|
||
private function stopDispatching(): void { | ||
$this->pendingPayload = null; | ||
$this->isDispatching = false; | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
use type Nazg\Dispatcher\Dispatcher; | ||
use type Facebook\HackTest\HackTest; | ||
use function Facebook\FBExpect\expect; | ||
|
||
final class DispatcherTest extends HackTest { | ||
|
||
private int $call = 0; | ||
|
||
public async function testShouldReturnInctID(): Awaitable<void> { | ||
$dispatcher = new Dispatcher(); | ||
$id = $dispatcher->register(($v) ==> { | ||
$v; | ||
}); | ||
expect($id)->toBeSame('ID_2'); | ||
$id = $dispatcher->register(($v) ==> { | ||
$v; | ||
}); | ||
expect($id)->toBeSame('ID_3'); | ||
} | ||
|
||
public async function testShouldThrowInvariantException(): Awaitable<void> { | ||
$dispatcher = new Dispatcher(); | ||
expect(() ==> $dispatcher->unregister('ID_1')) | ||
->toThrow(InvariantException::class); | ||
} | ||
|
||
public async function testShouldExecuteCallbacks(): Awaitable<void> { | ||
$dispatcher = new Dispatcher(); | ||
$id = $dispatcher->register(($_) ==> { | ||
$this->call = $this->call + 1; | ||
}); | ||
$payload = dict[]; | ||
$dispatcher->dispatch($payload); | ||
expect($this->call)->toBeSame(1); | ||
$dispatcher->unregister($id); | ||
$dispatcher->register(($_) ==> { | ||
$this->call = $this->call + 1; | ||
}); | ||
$dispatcher->dispatch($payload); | ||
expect($this->call)->toBeSame(2); | ||
} | ||
|
||
public async function testShouldWaitForCallbacks(): Awaitable<void> { | ||
$dispatcher = new Dispatcher(); | ||
$token = $dispatcher->register(($_) ==> { | ||
$this->call = $this->call + 1; | ||
}); | ||
$callback = ($v) ==> { | ||
$this->call = $this->call + 1; | ||
}; | ||
$dispatcher->register(($dict) ==> { | ||
$dispatcher->waitFor(vec[$token]); | ||
expect($this->call)->toBeSame(1); | ||
$callback($dict); | ||
}); | ||
$payload = dict[]; | ||
$dispatcher->dispatch($payload); | ||
expect($this->call)->toBeSame(2); | ||
} | ||
|
||
public async function testShouldWaitForAsyncCallbacks(): Awaitable<void> { | ||
$dispatcher = new Dispatcher(); | ||
$callback = ($v) ==> { | ||
$this->call = $this->call + 1; | ||
}; | ||
|
||
$token = await async { | ||
return $dispatcher->register(($_) ==> { | ||
$this->call = $this->call + 1; | ||
}); | ||
}; | ||
|
||
await async { | ||
$dispatcher->register(($dict) ==> { | ||
$dispatcher->waitFor(vec[$token]); | ||
expect($this->call)->toBeSame(1); | ||
$callback($dict); | ||
}); | ||
}; | ||
$payload = dict[]; | ||
$dispatcher->dispatch($payload); | ||
expect($this->call)->toBeSame(2); | ||
} | ||
|
||
public async function testShouldThrowDispatching(): Awaitable<void> { | ||
$dispatcher = new Dispatcher(); | ||
$dispatcher->register(($payload) ==> { | ||
$this->call = $this->call + 1; | ||
$dispatcher->dispatch($payload); | ||
}); | ||
$payload = dict[]; | ||
expect(() ==> $dispatcher->dispatch($payload)) | ||
->toThrow(InvariantException::class); | ||
expect($this->call)->toBeSame(1); | ||
} | ||
|
||
public async function testShouldThrowWaitFor(): Awaitable<void> { | ||
$dispatcher = new Dispatcher(); | ||
$token = $dispatcher->register(($payload) ==> { | ||
$this->call = $this->call + 1; | ||
}); | ||
expect(() ==> $dispatcher->waitFor(vec[$token])) | ||
->toThrow(InvariantException::class); | ||
} | ||
|
||
public async function testShouldThrowWaitForInvalidToken(): Awaitable<void> { | ||
$dispatcher = new Dispatcher(); | ||
$token = '1111111'; | ||
$dispatcher->register(($_) ==> { | ||
$dispatcher->waitFor(vec[$token]); | ||
}); | ||
expect(() ==> $dispatcher->dispatch(dict[])) | ||
->toThrow(OutOfBoundsException::class); | ||
} | ||
|
||
public async function afterEachTestAsync(): Awaitable<void> { | ||
$this->call = 0; | ||
} | ||
} |
Oops, something went wrong.