diff --git a/src/Actor/Machine.php b/src/Actor/Machine.php index 51d331e..e146e6d 100644 --- a/src/Actor/Machine.php +++ b/src/Actor/Machine.php @@ -217,14 +217,37 @@ public function send( */ public function persist(): ?State { + // Retrieve the previous context from the definition's config, or set it to an empty array if not set. + $incrementalContext = $this->definition->initializeContextFromState()->toArray(); + + // Get the last event from the state's history. + $lastHistoryEvent = $this->state->history->last(); + MachineEvent::upsert( - values: $this->state->history->map(fn (MachineEvent $machineEvent) => array_merge($machineEvent->toArray(), [ - 'created_at' => $machineEvent->created_at->toDateTimeString(), - 'machine_value' => json_encode($machineEvent->machine_value, JSON_THROW_ON_ERROR), - 'payload' => json_encode($machineEvent->payload, JSON_THROW_ON_ERROR), - 'context' => json_encode($machineEvent->context, JSON_THROW_ON_ERROR), - 'meta' => json_encode($machineEvent->meta, JSON_THROW_ON_ERROR), - ]))->toArray(), + values: $this->state->history->map(function (MachineEvent $machineEvent, int $index) use (&$incrementalContext, $lastHistoryEvent) { + // Get the context of the current machine event. + $changes = $machineEvent->context; + + // If the current machine event is not the last one, compare its context with the incremental context and get the differences. + if ($machineEvent->id !== $lastHistoryEvent->id && $index > 0) { + $changes = $this->arrayRecursiveDiff($changes, $incrementalContext); + } + + // If there are changes, update the incremental context to the current event's context. + if (!empty($changes)) { + $incrementalContext = $this->arrayRecursiveMerge($incrementalContext, $machineEvent->context); + } + + $machineEvent->context = $changes; + + return array_merge($machineEvent->toArray(), [ + 'created_at' => $machineEvent->created_at->toDateTimeString(), + 'machine_value' => json_encode($machineEvent->machine_value, JSON_THROW_ON_ERROR), + 'payload' => json_encode($machineEvent->payload, JSON_THROW_ON_ERROR), + 'context' => json_encode($machineEvent->context, JSON_THROW_ON_ERROR), + 'meta' => json_encode($machineEvent->meta, JSON_THROW_ON_ERROR), + ]); + })->toArray(), uniqueBy: ['id'] ); @@ -477,4 +500,48 @@ public function result(): mixed $arguments ?? null, ); } + + // region Private Methods + /** + * Compares two arrays recursively and returns the difference. + */ + private function arrayRecursiveDiff(array $array1, array $array2): array + { + $difference = []; + foreach ($array1 as $key => $value) { + if (is_array($value)) { + if (!isset($array2[$key]) || !is_array($array2[$key])) { + $difference[$key] = $value; + } else { + $new_diff = $this->arrayRecursiveDiff($value, $array2[$key]); + if (!empty($new_diff)) { + $difference[$key] = $new_diff; + } + } + } elseif (!array_key_exists($key, $array2) || $array2[$key] !== $value) { + $difference[$key] = $value; + } + } + + return $difference; + } + + /** + * Merges two arrays recursively. + */ + protected function arrayRecursiveMerge(array $array1, array $array2): array + { + $merged = $array1; + + foreach ($array2 as $key => &$value) { + if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) { + $merged[$key] = $this->arrayRecursiveMerge($merged[$key], $value); + } else { + $merged[$key] = $value; + } + } + + return $merged; + } + // endregion } diff --git a/tests/EventStoreTest.php b/tests/EventStoreTest.php index f078426..903640e 100644 --- a/tests/EventStoreTest.php +++ b/tests/EventStoreTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Tarfinlabs\EventMachine\Actor\Machine; use Tarfinlabs\EventMachine\ContextManager; use Tarfinlabs\EventMachine\Definition\EventDefinition; use Tarfinlabs\EventMachine\Definition\MachineDefinition; @@ -159,3 +160,66 @@ 'in.transition.active.MUT.fail', ]); }); + +it('stores incremental context', function (): void { + $machine = Machine::create([ + 'config' => [ + 'id' => 'traffic_light', + 'initial' => 'green', + 'context' => [ + 'count' => 1, + 'value' => 'test', + ], + 'states' => [ + 'green' => [ + 'on' => [ + 'GREEN_TIMER' => [ + 'target' => 'yellow', + 'actions' => 'changeCount', + ], + ], + ], + 'yellow' => [ + 'on' => [ + 'RED_TIMER' => [ + 'target' => 'red', + 'actions' => 'changeValue', + ], + ], + ], + 'red' => [], + ], + ], + 'behavior' => [ + 'actions' => [ + 'changeCount' => function (ContextManager $context): void { + $context->set('count', $context->get('count') + 1); + }, + 'changeValue' => function (ContextManager $context): void { + $context->set('value', 'retry'); + }, + ], + ], + ]); + + $machine->send(event: [ + 'type' => 'GREEN_TIMER', + ]); + + $newState = $machine->send(event: [ + 'type' => 'RED_TIMER', + ]); + + expect($newState->history) + ->whereNotIn('type', [ + 'traffic_light.start', + 'traffic_light.action.changeCount.finish', + 'traffic_light.action.changeValue.finish', + 'traffic_light.state.red.entry.finish', + ])->each(fn ($event) => $event->context->toEqual([])) + ->first()->context->toEqual(['data' => ['count' => 1, 'value' => 'test']]) + ->where('type', 'traffic_light.action.changeCount.finish')->first()->context->toEqual(['data' => ['count' => 2]]) + ->where('type', 'traffic_light.action.changeValue.finish')->first()->context->toEqual(['data' => ['value' => 'retry']]) + ->last()->context->toEqual(['data' => ['count' => 2, 'value' => 'retry']]); + +});