Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add fast publishing method #85

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/Debug/TraceableHub.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Symfony\Component\Mercure\Jwt\TokenProviderInterface;
use Symfony\Component\Mercure\Update;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\Service\ResetInterface;

/**
Expand Down Expand Up @@ -74,6 +75,36 @@ public function publish(Update $update): string
return $content;
}

public function publishFast(Update $update, ?string $token = null): ResponseInterface
{
$this->stopwatch->start(__CLASS__);
$content = $this->hub->publishFast($update, $token);

$e = $this->stopwatch->stop(__CLASS__);
$this->messages[] = [
'object' => $update,
'duration' => $e->getDuration(),
'memory' => $e->getMemory(),
];

return $content;
}

public function publishBatch($updates, bool $fireAndForget = false): array
{
$this->stopwatch->start(__CLASS__);
$content = $this->hub->publishBatch($updates);

$e = $this->stopwatch->stop(__CLASS__);
$this->messages[] = [
'object' => $updates,
'duration' => $e->getDuration(),
'memory' => $e->getMemory(),
];

return $content;
}

public function reset(): void
{
$this->messages = [];
Expand Down
49 changes: 44 additions & 5 deletions src/Hub.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Symfony\Component\Mercure\Jwt\TokenProviderInterface;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

/**
* @author Saif Eddin Gmati <azjezz@protonmail.com>
Expand Down Expand Up @@ -82,6 +83,21 @@ public function getFactory(): ?TokenFactoryInterface
* {@inheritDoc}
*/
public function publish(Update $update): string
{
$jwt = $this->getProvider()->getJwt();
$this->validateJwt($jwt);

try {
return $this->publishFast($update, $jwt)->getContent();
} catch (ExceptionInterface $exception) {
throw new Exception\RuntimeException('Failed to send an update.', 0, $exception);
}
}

/**
* {@inheritDoc}
*/
public function publishFast(Update $update, ?string $token = null): ResponseInterface
{
$postData = [
'topic' => $update->getTopics(),
Expand All @@ -92,14 +108,37 @@ public function publish(Update $update): string
'retry' => $update->getRetry(),
];

if (!$token) {
$token = $this->getProvider()->getJwt();
$this->validateJwt($token);
}

return $this->httpClient->request('POST', $this->getUrl(), [
'auth_bearer' => $token,
'body' => Internal\QueryBuilder::build($postData),
]);
}

/**
* {@inheritDoc}
*/
public function publishBatch($updates, bool $fireAndForget = false): array
{
$jwt = $this->getProvider()->getJwt();
$this->validateJwt($jwt);

try {
return $this->httpClient->request('POST', $this->getUrl(), [
'auth_bearer' => $jwt,
'body' => Internal\QueryBuilder::build($postData),
])->getContent();
$requests = [];
foreach ($updates as $update) {
$requests[] = $this->publishFast($update, $jwt);
}
if ($fireAndForget) {
return [];
}

return array_map(function ($val) {
return $val->getContent();
}, $requests);
} catch (ExceptionInterface $exception) {
throw new Exception\RuntimeException('Failed to send an update.', 0, $exception);
}
Expand All @@ -118,7 +157,7 @@ public function publish(Update $update): string
private function validateJwt(string $jwt): void
{
if (!preg_match('/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/', $jwt)) {
throw new Exception\InvalidArgumentException('The provided JWT is not valid');
throw new Exception\InvalidArgumentException('The provided JWT is not valid.');
}
}
}
13 changes: 13 additions & 0 deletions src/HubInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use Symfony\Component\Mercure\Jwt\TokenFactoryInterface;
use Symfony\Component\Mercure\Jwt\TokenProviderInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

/**
* @author Saif Eddin Gmati <azjezz@protonmail.com>
Expand Down Expand Up @@ -50,4 +51,16 @@ public function getFactory(): ?TokenFactoryInterface;
* Publish an update to this Hub.
*/
public function publish(Update $update): string;

/**
* Publish an update to this Hub.
*/
public function publishFast(Update $update, ?string $token = null): ResponseInterface;

/**
* Publish updates to this Hub.
*
* @param iterable<Update> $updates
*/
public function publishBatch($updates, bool $fireAndForget = false): array;
}
24 changes: 23 additions & 1 deletion src/MockHub.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use Symfony\Component\Mercure\Jwt\TokenFactoryInterface;
use Symfony\Component\Mercure\Jwt\TokenProviderInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

final class MockHub implements HubInterface
{
Expand All @@ -25,7 +26,7 @@ final class MockHub implements HubInterface
private $publicUrl;

/**
* @param (callable(Update): string) $publisher
* @param (callable(Update): ResponseInterface) $publisher
*/
public function __construct(
string $url,
Expand Down Expand Up @@ -62,7 +63,28 @@ public function getFactory(): ?TokenFactoryInterface
}

public function publish(Update $update): string
{
return ($this->publisher)($update)->getContent();
}

public function publishFast(Update $update, ?string $token = null): ResponseInterface
{
return ($this->publisher)($update);
}

public function publishBatch($updates, bool $fireAndForget = false): array
{
$requests = [];
$token = null;
foreach ($updates as $update) {
$requests[] = $this->publishFast($update, $token);
}
if ($fireAndForget) {
return [];
}

return array_map(function ($val) {
return $val->getContent();
}, $requests);
}
}
29 changes: 21 additions & 8 deletions tests/AuthorizationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use Lcobucci\JWT\Signer\Key\InMemory;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mercure\Authorization;
use Symfony\Component\Mercure\Exception\RuntimeException;
Expand All @@ -24,6 +25,7 @@
use Symfony\Component\Mercure\Jwt\TokenFactoryInterface;
use Symfony\Component\Mercure\MockHub;
use Symfony\Component\Mercure\Update;
use Symfony\Contracts\HttpClient\ResponseInterface;

/**
* @author Kévin Dunglas <kevin@dunglas.fr>
Expand All @@ -39,7 +41,9 @@ public function testJwtLifetime(): void
$registry = new HubRegistry(new MockHub(
'https://example.com/.well-known/mercure',
new StaticTokenProvider('foo.bar.baz'),
function (Update $u): string { return 'dummy'; },
function (Update $u): ResponseInterface {
return new MockResponse('dummy');
},
new LcobucciFactory('secret', 'hmac.sha256', 3600)
));

Expand All @@ -57,13 +61,14 @@ public function testSetCookie(): void
$tokenFactory
->expects($this->once())
->method('create')
->with($this->equalTo(['foo']), $this->equalTo(['bar']), $this->arrayHasKey('x-foo'))
;
->with($this->equalTo(['foo']), $this->equalTo(['bar']), $this->arrayHasKey('x-foo'));

$registry = new HubRegistry(new MockHub(
'https://example.com/.well-known/mercure',
new StaticTokenProvider('foo.bar.baz'),
function (Update $u): string { return 'dummy'; },
function (Update $u): ResponseInterface {
return new MockResponse('dummy');
},
$tokenFactory
));

Expand All @@ -81,7 +86,9 @@ public function testClearCookie(): void
$registry = new HubRegistry(new MockHub(
'https://example.com/.well-known/mercure',
new StaticTokenProvider('foo.bar.baz'),
function (Update $u): string { return 'dummy'; },
function (Update $u): ResponseInterface {
return new MockResponse('dummy');
},
new class() implements TokenFactoryInterface {
public function create(array $subscribe = [], array $publish = [], array $additionalClaims = []): string
{
Expand Down Expand Up @@ -111,7 +118,9 @@ public function testApplicableCookieDomains(?string $expected, string $hubUrl, s
$registry = new HubRegistry(new MockHub(
$hubUrl,
new StaticTokenProvider('foo.bar.baz'),
function (Update $u): string { return 'dummy'; },
function (Update $u): ResponseInterface {
return new MockResponse('dummy');
},
new LcobucciFactory('secret', 'hmac.sha256', 3600)
));

Expand Down Expand Up @@ -143,7 +152,9 @@ public function testNonApplicableCookieDomains(string $hubUrl, string $requestUr
$registry = new HubRegistry(new MockHub(
$hubUrl,
new StaticTokenProvider('foo.bar.baz'),
function (Update $u): string { return 'dummy'; },
function (Update $u): ResponseInterface {
return new MockResponse('dummy');
},
new LcobucciFactory('secret', 'hmac.sha256', 3600)
));

Expand All @@ -168,7 +179,9 @@ public function testSetMultipleCookies(): void
$registry = new HubRegistry(new MockHub(
'https://example.com/.well-known/mercure',
new StaticTokenProvider('foo.bar.baz'),
function (Update $u): string { return 'dummy'; },
function (Update $u): ResponseInterface {
return new MockResponse('dummy');
},
new class() implements TokenFactoryInterface {
public function create(array $subscribe = [], array $publish = [], array $additionalClaims = []): string
{
Expand Down
30 changes: 23 additions & 7 deletions tests/HubRegistryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,46 @@
namespace Symfony\Component\Mercure\Tests;

use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Component\Mercure\Exception\InvalidArgumentException;
use Symfony\Component\Mercure\HubRegistry;
use Symfony\Component\Mercure\Jwt\StaticTokenProvider;
use Symfony\Component\Mercure\MockHub;
use Symfony\Contracts\HttpClient\ResponseInterface;

class HubRegistryTest extends TestCase
{
public function testGetHubByName(): void
{
$fooHub = new MockHub('fooUrl', new StaticTokenProvider('fooToken'), static function (): string { return 'foo'; });
$barHub = new MockHub('barUrl', new StaticTokenProvider('barToken'), static function (): string { return 'bar'; });
$fooHub = new MockHub('fooUrl', new StaticTokenProvider('fooToken'), static function (): ResponseInterface {
return new MockResponse('foo');
});
$barHub = new MockHub('barUrl', new StaticTokenProvider('barToken'), static function (): ResponseInterface {
return new MockResponse('bar');
});
$registry = new HubRegistry($fooHub, ['foo' => $fooHub, 'bar' => $barHub]);

$this->assertSame($fooHub, $registry->getHub('foo'));
}

public function testGetDefaultHub(): void
{
$fooHub = new MockHub('fooUrl', new StaticTokenProvider('fooToken'), static function (): string { return 'foo'; });
$barHub = new MockHub('barUrl', new StaticTokenProvider('barToken'), static function (): string { return 'bar'; });
$fooHub = new MockHub('fooUrl', new StaticTokenProvider('fooToken'), static function (): ResponseInterface {
return new MockResponse('foo');
});
$barHub = new MockHub('barUrl', new StaticTokenProvider('barToken'), static function (): ResponseInterface {
return new MockResponse('bar');
});
$registry = new HubRegistry($fooHub, ['foo' => $fooHub, 'bar' => $barHub]);

$this->assertSame($fooHub, $registry->getHub());
}

public function testGetMissingHubThrows(): void
{
$fooHub = new MockHub('fooUrl', new StaticTokenProvider('fooToken'), static function (): string { return 'foo'; });
$fooHub = new MockHub('fooUrl', new StaticTokenProvider('fooToken'), static function (): ResponseInterface {
return new MockResponse('foo');
});
$registry = new HubRegistry($fooHub, ['foo' => $fooHub]);

$this->expectException(InvalidArgumentException::class);
Expand All @@ -50,8 +62,12 @@ public function testGetMissingHubThrows(): void

public function testGetAllHubs(): void
{
$fooHub = new MockHub('fooUrl', new StaticTokenProvider('fooToken'), static function (): string { return 'foo'; });
$barHub = new MockHub('barUrl', new StaticTokenProvider('barToken'), static function (): string { return 'bar'; });
$fooHub = new MockHub('fooUrl', new StaticTokenProvider('fooToken'), static function (): ResponseInterface {
return new MockResponse('foo');
});
$barHub = new MockHub('barUrl', new StaticTokenProvider('barToken'), static function (): ResponseInterface {
return new MockResponse('bar');
});
$registry = new HubRegistry($fooHub, ['foo' => $fooHub, 'bar' => $barHub]);

$this->assertSame(['foo' => $fooHub, 'bar' => $barHub], $registry->all());
Expand Down
10 changes: 7 additions & 3 deletions tests/Twig/MercureExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace Symfony\Component\Mercure\Tests\Twig;

use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
Expand All @@ -24,6 +25,7 @@
use Symfony\Component\Mercure\MockHub;
use Symfony\Component\Mercure\Twig\MercureExtension;
use Symfony\Component\Mercure\Update;
use Symfony\Contracts\HttpClient\ResponseInterface;

/**
* @author Kévin Dunglas <kevin@dunglas.fr>
Expand All @@ -35,7 +37,9 @@ public function testMercure(): void
$registry = new HubRegistry(new MockHub(
'https://example.com/.well-known/mercure',
new StaticTokenProvider('foo.bar.baz'),
function (Update $u): string { return 'dummy'; },
function (Update $u): ResponseInterface {
return new MockResponse('dummy');
},
$this->createMock(TokenFactoryInterface::class)
));

Expand All @@ -56,8 +60,8 @@ public function testMercureLastEventId(): void
$registry = new HubRegistry(new MockHub(
'https://example.com/.well-known/mercure',
new StaticTokenProvider('foo.bar.baz'),
function (Update $u): string {
return 'dummy';
function (Update $u): ResponseInterface {
return new MockResponse('dummy');
},
$this->createMock(TokenFactoryInterface::class)
));
Expand Down