Skip to content
Merged
7 changes: 4 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"dereuromark/composer-prefer-lowest": "^0.1.10",
"doctrine/annotations": "^1.14.4 || ^2.0.2",
"internal/dload": "^1.0",
"jetbrains/phpstorm-attributes": "dev-master@dev",
"jetbrains/phpstorm-attributes": "dev-master",
"laminas/laminas-code": "^4.16",
"phpunit/phpunit": "^10.5.41",
"spiral/code-style": "~2.1.2",
Expand All @@ -77,8 +77,9 @@
}
},
"suggest": {
"doctrine/annotations": "For Doctrine metadata driver support",
"ext-grpc": "For Client calls"
"ext-grpc": "For Client calls",
"ext-protobuf": "For better performance",
"buggregator/trap": "For better debugging"
},
"scripts": {
"post-update-cmd": "Temporal\\Worker\\Transport\\RoadRunnerVersionChecker::postUpdate",
Expand Down
24 changes: 10 additions & 14 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -600,23 +600,16 @@
</RedundantConditionGivenDocblockType>
</file>
<file src="src/Internal/Declaration/WorkflowInstance.php">
<MissingClosureReturnType>
<code><![CDATA[function (QueryInput $input) use ($fn) {]]></code>
</MissingClosureReturnType>
<LessSpecificImplementedReturnType>
<code><![CDATA[UpdateHandler|null]]></code>
</LessSpecificImplementedReturnType>
<PropertyNotSetInConstructor>
<code><![CDATA[$queryExecutor]]></code>
<code><![CDATA[$updateExecutor]]></code>
<code><![CDATA[$updateValidator]]></code>
</PropertyNotSetInConstructor>
<PropertyTypeCoercion>
<code><![CDATA[$this->queryHandlers]]></code>
<code><![CDATA[$this->signalHandlers]]></code>
</PropertyTypeCoercion>
</file>
<file src="src/Internal/Declaration/WorkflowInstance/SignalQueue.php">
<ArgumentTypeCoercion>
<code><![CDATA[$signal]]></code>
</ArgumentTypeCoercion>
<MissingConstructor>
<code><![CDATA[$onSignal]]></code>
</MissingConstructor>
Expand Down Expand Up @@ -1139,9 +1132,6 @@
</UnevaluatedCode>
</file>
<file src="src/Internal/Workflow/Process/Process.php">
<MissingClosureParamType>
<code><![CDATA[$result]]></code>
</MissingClosureParamType>
<PropertyNotSetInConstructor>
<code><![CDATA[Process]]></code>
</PropertyNotSetInConstructor>
Expand Down Expand Up @@ -1473,9 +1463,15 @@
<InvalidReturnStatement>
<code><![CDATA[self::getCurrentContext()->newActivityStub($class, $options)]]></code>
<code><![CDATA[self::getCurrentContext()->registerQuery($queryType, $handler)]]></code>
<code><![CDATA[self::getCurrentContext()->registerSignal($queryType, $handler)]]></code>
<code><![CDATA[self::getCurrentContext()->registerQueryFallback($handler)]]></code>
<code><![CDATA[self::getCurrentContext()->registerSignal($name, $handler)]]></code>
<code><![CDATA[self::getCurrentContext()->registerSignalFallback($handler)]]></code>
<code><![CDATA[self::getCurrentContext()->registerUpdateFallback($handler, $validator)]]></code>
</InvalidReturnStatement>
<InvalidReturnType>
<code><![CDATA[ScopedContextInterface]]></code>
<code><![CDATA[ScopedContextInterface]]></code>
<code><![CDATA[ScopedContextInterface]]></code>
<code><![CDATA[ScopedContextInterface]]></code>
<code><![CDATA[ScopedContextInterface]]></code>
<code><![CDATA[T]]></code>
Expand Down
1 change: 1 addition & 0 deletions src/Interceptor/WorkflowInbound/QueryInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class QueryInput
* @internal Don't use the constructor. Use {@see self::with()} instead.
*/
public function __construct(
/** @var non-empty-string */
public readonly string $queryName,
public readonly ValuesInterface $arguments,
public readonly WorkflowInfo $info,
Expand Down
86 changes: 61 additions & 25 deletions src/Internal/Declaration/WorkflowInstance.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,35 +24,36 @@

/**
* @psalm-type QueryHandler = \Closure(QueryInput): mixed
* @psalm-type UpdateHandler = \Closure(UpdateInput, Deferred): PromiseInterface
* @psalm-type UpdateHandler = \Closure(UpdateInput, Deferred): mixed
* @psalm-type ValidateUpdateHandler = \Closure(UpdateInput): void
* @psalm-type QueryExecutor = \Closure(QueryInput, callable(ValuesInterface): mixed): mixed
* @psalm-type UpdateExecutor = \Closure(UpdateInput, callable(ValuesInterface): mixed, Deferred): PromiseInterface
* @psalm-type ValidateUpdateExecutor = \Closure(UpdateInput, callable(ValuesInterface): mixed): mixed
* @psalm-type ValidateUpdateExecutor = \Closure(UpdateInput, callable(ValuesInterface): mixed): void
* @psalm-type UpdateValidator = \Closure(UpdateInput, UpdateHandler): void
*
* @internal
*/
final class WorkflowInstance extends Instance implements WorkflowInstanceInterface
{
/**
* @var array<non-empty-string, QueryHandler>
*/
/** @var array<non-empty-string, QueryHandler> */
private array $queryHandlers = [];

/**
* @var array<non-empty-string, MethodHandler>
*/
/** @var null|QueryHandler */
private ?\Closure $queryFallbackHandler = null;

/** @var null|UpdateHandler */
private ?\Closure $updateFallbackHandler = null;

/** @var null|ValidateUpdateHandler */
private ?\Closure $updateFallbackValidator = null;

/** @var array<non-empty-string, MethodHandler> */
private array $signalHandlers = [];

/**
* @var array<non-empty-string, UpdateHandler>
*/
/** @var array<non-empty-string, UpdateHandler> */
private array $updateHandlers = [];

/**
* @var array<non-empty-string, null|ValidateUpdateHandler>
*/
/** @var array<non-empty-string, null|ValidateUpdateHandler> */
private array $validateUpdateHandlers = [];

private SignalQueue $signalQueue;
Expand Down Expand Up @@ -174,7 +175,7 @@ public function getSignalQueue(): SignalQueue
*/
public function findQueryHandler(string $name): ?\Closure
{
return $this->queryHandlers[$name] ?? null;
return $this->queryHandlers[$name] ?? $this->queryFallbackHandler;
}

/**
Expand All @@ -185,7 +186,7 @@ public function findQueryHandler(string $name): ?\Closure
*/
public function findUpdateHandler(string $name): ?\Closure
{
return $this->updateHandlers[$name] ?? null;
return $this->updateHandlers[$name] ?? $this->updateFallbackHandler;
}

/**
Expand All @@ -196,7 +197,11 @@ public function findUpdateHandler(string $name): ?\Closure
*/
public function findValidateUpdateHandler(string $name): ?\Closure
{
return $this->validateUpdateHandlers[$name] ?? null;
return $this->validateUpdateHandlers[$name] ?? (
\array_key_exists($name, $this->updateHandlers)
? null
: $this->updateFallbackValidator
);
}

/**
Expand All @@ -207,16 +212,13 @@ public function addQueryHandler(string $name, callable $handler): void
$fn = $this->createCallableHandler($handler);

$this->queryHandlers[$name] = $this->pipeline->with(
function (QueryInput $input) use ($fn) {
return ($this->queryExecutor)($input, $fn);
},
fn(QueryInput $input): mixed => ($this->queryExecutor)($input, $fn),
/** @see WorkflowInboundCallsInterceptor::handleQuery() */
'handleQuery',
)(...);
}

/**
* @param non-empty-string $name
* @throws \ReflectionException
*/
public function addUpdateHandler(string $name, callable $handler): void
Expand All @@ -233,7 +235,6 @@ function (UpdateInput $input, Deferred $deferred) use ($fn) {
}

/**
* @param non-empty-string $name
* @throws \ReflectionException
*/
public function addValidateUpdateHandler(string $name, callable $handler): void
Expand Down Expand Up @@ -263,15 +264,49 @@ public function getSignalHandler(string $name): \Closure
return fn(ValuesInterface $values) => $this->signalQueue->push($name, $values);
}

/**
* @throws \ReflectionException
*/
public function addSignalHandler(string $name, callable $handler): void
{
$this->signalHandlers[$name] = $this->createCallableHandler($handler);
$this->signalQueue->attach($name, $this->signalHandlers[$name]);
}

public function setSignalFallbackHandler(callable $handler): void
{
$this->signalQueue->setFallback($handler(...));
}

public function setQueryFallbackHandler(callable $handler): void
{
$this->queryFallbackHandler = $this->pipeline->with(
fn(QueryInput $input): mixed => ($this->queryExecutor)(
$input,
static fn(ValuesInterface $arguments): mixed => $handler($input->queryName, $arguments),
),
/** @see WorkflowInboundCallsInterceptor::handleQuery() */
'handleQuery',
)(...);
}

public function setUpdateFallbackHandler(callable $handler, ?callable $validator = null): void
{
$this->updateFallbackValidator = $validator === null
? null
: fn(UpdateInput $input): mixed => ($this->updateValidator)(
$input,
static fn(ValuesInterface $arguments): mixed => $validator($input->updateName, $arguments),
);

$this->updateFallbackHandler = $this->pipeline->with(
fn(UpdateInput $input, Deferred $deferred): mixed => ($this->updateExecutor)(
$input,
static fn(ValuesInterface $arguments): mixed => $handler($input->updateName, $arguments),
$deferred,
),
/** @see WorkflowInboundCallsInterceptor::handleUpdate() */
'handleUpdate',
)(...);
}

public function clearSignalQueue(): void
{
$this->signalQueue->clear();
Expand All @@ -290,6 +325,7 @@ public function destroy(): void
$this->updateValidator,
$this->prototype,
$this->pipeline,
$this->queryFallbackHandler,
);
parent::destroy();
}
Expand Down
78 changes: 60 additions & 18 deletions src/Internal/Declaration/WorkflowInstance/SignalQueue.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,54 +16,90 @@
/**
* @psalm-type Consumer = callable(ValuesInterface): mixed
*
* @psalm-type OnSignalCallable = callable(non-empty-string $name, callable $handler, ValuesInterface $arguments): void
* @psalm-type OnSignalCallable = \Closure(non-empty-string $name, Consumer $handler, ValuesInterface $arguments): void
*/
final class SignalQueue
{
/**
* @var array<string, list<ValuesInterface>>
* @var array<int, SignalQueueItem>
*/
private array $queue = [];

/**
* @var array<Consumer>
* @var array<non-empty-string, Consumer>
*/
private array $consumers = [];

/**
* @var OnSignalCallable
*/
private $onSignal;
private \Closure $onSignal;

/**
* A fallback consumer to handle signals when no consumer is attached.
*
* @var null|\Closure(non-empty-string, ValuesInterface): mixed
*/
private ?\Closure $fallbackConsumer = null;

/**
* @param non-empty-string $signal
*/
public function push(string $signal, ValuesInterface $values): void
{
if (isset($this->consumers[$signal])) {
($this->onSignal)($signal, $this->consumers[$signal], $values);
$this->consume($signal, $values, $this->consumers[$signal]);
return;
}

$this->queue[$signal][] = $values;
$this->flush($signal);
if ($this->fallbackConsumer !== null) {
$this->consumeFallback($signal, $values);
return;
}

$this->queue[] = new SignalQueueItem($signal, $values);
}

/**
* @param OnSignalCallable $handler
*/
public function onSignal(callable $handler): void
public function onSignal(\Closure $handler): void
{
$this->onSignal = $handler;
}

/**
* @param non-empty-string $signal
* @param Consumer $consumer
*/
public function attach(string $signal, callable $consumer): void
{
$this->consumers[$signal] = $consumer; // overwrite
$this->flush($signal);

foreach ($this->queue as $k => $item) {
if ($item->name === $signal) {
unset($this->queue[$k]);
$this->consume($signal, $item->values, $consumer);
}
}
}

/**
* @param \Closure(non-empty-string, ValuesInterface): mixed $consumer
*/
public function setFallback(\Closure $consumer): void
{
$this->fallbackConsumer = $consumer;

// Flush all signals that have no consumer
foreach ($this->queue as $k => $item) {
if (\array_key_exists($item->name, $this->consumers)) {
continue;
}

unset($this->queue[$k]);
$this->consumeFallback($item->name, $item->values);
}
}

public function clear(): void
Expand All @@ -72,18 +108,24 @@ public function clear(): void
}

/**
* @psalm-suppress UnusedVariable
* @param non-empty-string $signal
* @param Consumer $consumer
*/
private function flush(string $signal): void
private function consume(string $signal, ValuesInterface $values, callable $consumer): void
{
if (!isset($this->queue[$signal], $this->consumers[$signal])) {
return;
}
($this->onSignal)($signal, $consumer, $values);
}

while ($this->queue[$signal] !== []) {
$args = \array_shift($this->queue[$signal]);
/**
* @param non-empty-string $signal
*/
private function consumeFallback(string $signal, ValuesInterface $values): void
{
$handler = $this->fallbackConsumer;
\assert($handler !== null);

($this->onSignal)($signal, $this->consumers[$signal], $args);
}
// Wrap the fallback consumer to call interceptors
$consumer = static fn(ValuesInterface $values): mixed => $handler($signal, $values);
($this->onSignal)($signal, $consumer, $values);
}
}
Loading