fix: finalize graphql coroutine response isolation

This commit is contained in:
Chirag Aggarwal
2026-04-13 19:01:20 +05:30
parent f4f5494b85
commit fe02964ebd
4 changed files with 161 additions and 97 deletions
+1 -2
View File
@@ -72,8 +72,6 @@ $swooleAdapter = new Server(
container: $container,
);
$container->set('container', fn () => fn () => $swooleAdapter->getContainer());
$http = $swooleAdapter->getServer();
/**
@@ -533,6 +531,7 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
}
$requestContainer = $swooleAdapter->getContainer();
$requestContainer->set('container', fn () => $requestContainer);
$requestContainer->set('request', fn () => $request);
$requestContainer->set('response', fn () => $response);
+124 -81
View File
@@ -71,10 +71,15 @@ class Resolvers
*/
private static function getResolverContainer(Http $utopia): Container
{
/** @var callable(): Container $getContainer */
$getContainer = $utopia->getResource('container');
$container = $utopia->getResource('container');
return $getContainer();
if ($container instanceof Container || (\is_object($container) && \method_exists($container, 'get') && \method_exists($container, 'set'))) {
/** @var Container $container */
return $container;
}
/** @var callable(): Container $container */
return $container();
}
/**
@@ -140,33 +145,38 @@ class Resolvers
?Route $route,
): callable {
return static fn ($type, $args, $context, $info) => (function () use ($utopia, $route, $args) {
$lock = self::getLock($utopia);
return new Swoole(function (callable $resolve, callable $reject) use ($utopia, $route, $args, $lock) {
return new Swoole(function (callable $resolve, callable $reject) use ($utopia, $route, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$path = $route->getPath();
foreach ($args as $key => $value) {
if (\str_contains($path, '/:' . $key)) {
$path = \str_replace(':' . $key, $value, $path);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
prepareRequest: static function (Request $request) use ($route, $args): void {
$path = $route->getPath();
foreach ($args as $key => $value) {
if (\str_contains($path, '/:' . $key)) {
$path = \str_replace(':' . $key, $value, $path);
}
}
$request->setMethod($route->getMethod());
$request->setURI($path);
switch ($route->getMethod()) {
case 'GET':
$request->setQueryString($args);
break;
default:
$request->setPayload($args);
break;
}
}
}
$request->setMethod($route->getMethod());
$request->setURI($path);
switch ($route->getMethod()) {
case 'GET':
$request->setQueryString($args);
break;
default:
$request->setPayload($args);
break;
}
self::resolve($utopia, $request, $response, $lock, $resolve, $reject);
);
});
})();
}
@@ -209,17 +219,22 @@ class Resolvers
callable $url,
): callable {
return static fn ($type, $args, $context, $info) => (function () use ($utopia, $databaseId, $collectionId, $url, $args) {
$lock = self::getLock($utopia);
return new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args, $lock) {
return new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$request->setMethod('GET');
$request->setURI($url($databaseId, $collectionId, $args));
self::resolve($utopia, $request, $response, $lock, $resolve, $reject);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $args): void {
$request->setMethod('GET');
$request->setURI($url($databaseId, $collectionId, $args));
}
);
});
})();
}
@@ -242,22 +257,28 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => (function () use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$lock = self::getLock($utopia);
return new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args, $lock) {
return new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$request->setMethod('GET');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setQueryString($params($databaseId, $collectionId, $args));
$beforeResolve = function ($payload) {
return $payload['documents'];
};
self::resolve($utopia, $request, $response, $lock, $resolve, $reject, $beforeResolve);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
beforeResolve: $beforeResolve,
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $params, $args): void {
$request->setMethod('GET');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setQueryString($params($databaseId, $collectionId, $args));
}
);
});
})();
}
@@ -280,18 +301,23 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => (function () use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$lock = self::getLock($utopia);
return new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args, $lock) {
return new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$request->setMethod('POST');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setPayload($params($databaseId, $collectionId, $args));
self::resolve($utopia, $request, $response, $lock, $resolve, $reject);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $params, $args): void {
$request->setMethod('POST');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setPayload($params($databaseId, $collectionId, $args));
}
);
});
})();
}
@@ -314,18 +340,23 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => (function () use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$lock = self::getLock($utopia);
return new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args, $lock) {
return new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$request->setMethod('PATCH');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setPayload($params($databaseId, $collectionId, $args));
self::resolve($utopia, $request, $response, $lock, $resolve, $reject);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $params, $args): void {
$request->setMethod('PATCH');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setPayload($params($databaseId, $collectionId, $args));
}
);
});
})();
}
@@ -346,17 +377,22 @@ class Resolvers
callable $url,
): callable {
return static fn ($type, $args, $context, $info) => (function () use ($utopia, $databaseId, $collectionId, $url, $args) {
$lock = self::getLock($utopia);
return new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args, $lock) {
return new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$request->setMethod('DELETE');
$request->setURI($url($databaseId, $collectionId, $args));
self::resolve($utopia, $request, $response, $lock, $resolve, $reject);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $args): void {
$request->setMethod('DELETE');
$request->setURI($url($databaseId, $collectionId, $args));
}
);
});
})();
}
@@ -365,11 +401,10 @@ class Resolvers
* @param Http $utopia
* @param Request $request
* @param Response $response
* @param ResolverLock $lock
* @param callable $resolve
* @param callable $reject
* @param callable|null $beforeResolve
* @param callable|null $beforeReject
* @param callable|null $prepareRequest
* @return void
* @throws Exception
*/
@@ -377,23 +412,28 @@ class Resolvers
Http $utopia,
Request $request,
Response $response,
ResolverLock $lock,
callable $resolve,
callable $reject,
?callable $beforeResolve = null,
?callable $beforeReject = null,
?callable $prepareRequest = null,
): void {
// Drop json content type so post args are used directly
if (\str_starts_with($request->getHeader('content-type'), 'application/json')) {
$request->removeHeader('content-type');
}
$request = clone $request;
$resolverResponse = self::createResolverResponse($utopia);
$container = self::getResolverContainer($utopia);
$lock = self::getLock($utopia);
self::acquireLock($lock);
try {
$request = clone $request;
// Drop json content type so post args are used directly.
if (\str_starts_with($request->getHeader('content-type'), 'application/json')) {
$request->removeHeader('content-type');
}
if ($prepareRequest) {
$prepareRequest($request);
}
$resolverResponse = self::createResolverResponse($utopia);
$container = self::getResolverContainer($utopia);
$container->set('request', static fn () => $request);
$container->set('response', static fn () => $resolverResponse);
$resolverResponse->setContentType(Response::CONTENT_TYPE_NULL);
@@ -406,12 +446,18 @@ class Resolvers
self::mergeResponseSideEffects($resolverResponse, $response);
if ($resolverResponse->isSent()) {
$response
->setStatusCode($resolverResponse->getStatusCode())
->markSent();
$resolve(null);
return;
}
$payload = $resolverResponse->getPayload();
$statusCode = $resolverResponse->getStatusCode();
} catch (\Throwable $e) {
if ($beforeReject) {
$e = $beforeReject($e);
}
$reject($e);
return;
} finally {
@@ -419,9 +465,6 @@ class Resolvers
}
if ($statusCode < 200 || $statusCode >= 400) {
if ($beforeReject) {
$payload = $beforeReject($payload);
}
$reject(new GQLException(
message: $payload['message'],
code: $statusCode
+24 -14
View File
@@ -19,8 +19,7 @@ abstract class Promise
return;
}
$resolve = function ($value) {
$this->setResult($value);
$this->setState(self::STATE_FULFILLED);
$this->setState($this->setResult($value));
};
$reject = function ($value) {
$this->setResult($value);
@@ -106,6 +105,11 @@ abstract class Promise
}
$callable = $this->isFulfilled() ? $onFulfilled : $onRejected;
if (!\is_callable($callable)) {
if ($this->isRejected()) {
$reject($this->result);
return;
}
$resolve($this->result);
return;
}
@@ -126,30 +130,36 @@ abstract class Promise
abstract public static function all(iterable $promises): self;
/**
* Set resolved result
* Set the resolved result, adopting nested promises while preserving
* whether the adopted promise fulfilled or rejected.
*
* @param mixed $value
* @return void
* @return int
*/
protected function setResult(mixed $value): void
protected function setResult(mixed $value): int
{
if (!\is_callable([$value, 'then'])) {
$this->result = $value;
return;
return self::STATE_FULFILLED;
}
$resolved = false;
$state = self::STATE_PENDING;
$callable = function ($value) use (&$resolved) {
$this->setResult($value);
$resolved = true;
};
$value->then(
function ($value) use (&$state) {
$state = $this->setResult($value);
},
function ($value) use (&$state) {
$this->result = $value;
$state = self::STATE_REJECTED;
}
);
$value->then($callable, $callable);
while (!$resolved) {
while ($state === self::STATE_PENDING) {
usleep(25000);
}
return $state;
}
/**
+12
View File
@@ -613,6 +613,8 @@ class Response extends SwooleResponse
throw new \Exception('Response body is not a valid JSON object.');
}
$this->payload = \is_array($data) ? $data : (array) $data;
$this
->setContentType(Response::CONTENT_TYPE_JSON, self::CHARSET_UTF8)
->send(\json_encode($data, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
@@ -637,6 +639,16 @@ class Response extends SwooleResponse
return $this;
}
/**
* Mark the response as already sent so later callers do not attempt to
* write a second payload to the same underlying Swoole response.
*/
public function markSent(): static
{
$this->sent = true;
return $this;
}
/**
* Function to add a response filter, the order of filters are first in - first out.
*