From 3cc1f9938dd8a99672d713c52e80d5ca9241aa18 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 23 Aug 2024 14:29:51 -0400 Subject: [PATCH 01/16] Performance optimizations --- .../Autodiscovery/AppliesToState.php | 12 ++++++---- src/Attributes/Autodiscovery/StateId.php | 10 ++++---- src/Lifecycle/EventStore.php | 4 +++- src/Lifecycle/StateManager.php | 23 ++++++++++++++++--- src/State.php | 6 +---- 5 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/Attributes/Autodiscovery/AppliesToState.php b/src/Attributes/Autodiscovery/AppliesToState.php index 21aa9366..32dd227e 100644 --- a/src/Attributes/Autodiscovery/AppliesToState.php +++ b/src/Attributes/Autodiscovery/AppliesToState.php @@ -24,23 +24,25 @@ public function __construct( } } - public function discoverState(Event $event, StateManager $manager): array + public function discoverState(Event $event, StateManager $manager): State|array { $property = $this->getStateIdProperty($event); $id = $event->{$property}; + if (! is_array($id)) { + $this->alias ??= $this->inferAliasFromVariableName($property); + } + // If the ID hasn't been set yet, we'll automatically set one if ($id === null && $this->autofill) { $id = snowflake_id(); $event->{$property} = $id; + + return $manager->make($id, $this->state_type); } // TODO: Check type of data - if (! is_array($id)) { - $this->alias ??= $this->inferAliasFromVariableName($property); - } - return collect(Arr::wrap($id)) ->map(fn ($id) => $manager->load($id, $this->state_type)) ->all(); diff --git a/src/Attributes/Autodiscovery/StateId.php b/src/Attributes/Autodiscovery/StateId.php index a45524ae..d6453258 100644 --- a/src/Attributes/Autodiscovery/StateId.php +++ b/src/Attributes/Autodiscovery/StateId.php @@ -23,18 +23,20 @@ public function __construct( } } - public function discoverState(Event $event, StateManager $manager): array + public function discoverState(Event $event, StateManager $manager): State|array { $id = $this->property->getValue($event); + if (! is_array($id)) { + $this->alias ??= $this->inferAliasFromVariableName($this->property->getName()); + } + // If the ID hasn't been set yet, we'll automatically set one if ($id === null && $this->autofill) { $id = snowflake_id(); $this->property->setValue($event, $id); - } - if (! is_array($id)) { - $this->alias ??= $this->inferAliasFromVariableName($this->property->getName()); + return $manager->make($id, $this->state_type); } return collect(Arr::wrap($id)) diff --git a/src/Lifecycle/EventStore.php b/src/Lifecycle/EventStore.php index 5a24c5f1..b68e5c1d 100644 --- a/src/Lifecycle/EventStore.php +++ b/src/Lifecycle/EventStore.php @@ -59,12 +59,14 @@ protected function readEvents( ->where('state_type', $state::class) ->when($after_id, fn (Builder $query) => $query->whereRelation('event', 'id', '>', Id::from($after_id))) ->lazyById() + ->remember() ->map(fn (VerbStateEvent $pivot) => $pivot->event); } return VerbEvent::query() ->when($after_id, fn (Builder $query) => $query->where('id', '>', Id::from($after_id))) - ->lazyById(); + ->lazyById() + ->remember(); } /** @param Event[] $events */ diff --git a/src/Lifecycle/StateManager.php b/src/Lifecycle/StateManager.php index 45b2754a..f52da456 100644 --- a/src/Lifecycle/StateManager.php +++ b/src/Lifecycle/StateManager.php @@ -4,6 +4,7 @@ use Glhd\Bits\Bits; use Ramsey\Uuid\UuidInterface; +use ReflectionClass; use Symfony\Component\Uid\AbstractUid; use Thunk\Verbs\Contracts\StoresEvents; use Thunk\Verbs\Contracts\StoresSnapshots; @@ -48,11 +49,9 @@ public function load(Bits|UuidInterface|AbstractUid|int|string $id, string $type throw new UnexpectedValueException(sprintf('Expected State <%d> to be of type "%s" but got "%s"', $id, class_basename($type), class_basename($state))); } } else { - $state = $type::make(); - $state->id = $id; + $state = $this->make($id, $type); } - $this->remember($state); $this->reconstitute($state); return $state; @@ -79,6 +78,24 @@ public function singleton(string $type): State return $state; } + /** + * @template TState of State + * + * @param class-string $type + * @return TState + */ + public function make(Bits|UuidInterface|AbstractUid|int|string $id, string $type): State + { + // State::__construct() auto-registers the state with the StateManager, + // so we need to skip the constructor until we've already set the ID. + + $state = (new ReflectionClass($type))->newInstanceWithoutConstructor(); + $state->id = Id::from($id); + $state->__construct(); + + return $this->remember($state); + } + public function writeSnapshots(): bool { return $this->snapshots->write($this->states->values()); diff --git a/src/State.php b/src/State.php index 0ddfac5c..f06f5b8c 100644 --- a/src/State.php +++ b/src/State.php @@ -30,11 +30,7 @@ public static function make(...$args): static $args = $args[0]; } - $state = app(Serializer::class)->deserialize(static::class, $args, call_constructor: true); - - app(StateManager::class)->register($state); - - return $state; + return app(Serializer::class)->deserialize(static::class, $args, call_constructor: true); } /** @return StateFactory */ From 8102edfa79bc8bf06d118a5b289635073c10bd48 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 23 Aug 2024 14:33:17 -0400 Subject: [PATCH 02/16] Update Metadata.php --- src/Metadata.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Metadata.php b/src/Metadata.php index fff4c08e..c72c76c9 100644 --- a/src/Metadata.php +++ b/src/Metadata.php @@ -11,8 +11,6 @@ class Metadata implements ArrayAccess public function __construct(array $data = []) { - $this->extra = new Collection; - $this->merge($data); } @@ -39,26 +37,36 @@ public function merge(iterable $data): static public function __get(string $name) { + $this->extra ??= new Collection(); + return $this->extra->get($name); } public function __set(string $name, $value): void { + $this->extra ??= new Collection(); + $this->extra->put($name, $value); } public function __isset(string $name): bool { + $this->extra ??= new Collection(); + return $this->extra->has($name); } public function __unset(string $name): void { + $this->extra ??= new Collection(); + $this->extra->forget($name); } public function __sleep(): array { + $this->extra ??= new Collection(); + return $this->extra->toArray(); } From 27b2b6ffa0afee1a7f9a77d41ac8d318abab081d Mon Sep 17 00:00:00 2001 From: inxilpro Date: Fri, 23 Aug 2024 18:33:41 +0000 Subject: [PATCH 03/16] Fix styling --- src/Metadata.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Metadata.php b/src/Metadata.php index c72c76c9..a851ff6b 100644 --- a/src/Metadata.php +++ b/src/Metadata.php @@ -37,35 +37,35 @@ public function merge(iterable $data): static public function __get(string $name) { - $this->extra ??= new Collection(); + $this->extra ??= new Collection; return $this->extra->get($name); } public function __set(string $name, $value): void { - $this->extra ??= new Collection(); + $this->extra ??= new Collection; $this->extra->put($name, $value); } public function __isset(string $name): bool { - $this->extra ??= new Collection(); + $this->extra ??= new Collection; return $this->extra->has($name); } public function __unset(string $name): void { - $this->extra ??= new Collection(); + $this->extra ??= new Collection; $this->extra->forget($name); } public function __sleep(): array { - $this->extra ??= new Collection(); + $this->extra ??= new Collection; return $this->extra->toArray(); } From 7384337aaceb23d147a728f6ce3e090919c66158 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 23 Aug 2024 17:20:47 -0400 Subject: [PATCH 04/16] more --- src/Attributes/Autodiscovery/StateId.php | 6 ++++++ src/Commands/ReplayCommand.php | 18 ++++++++++++++++-- src/Lifecycle/Broker.php | 12 +++++++++--- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/Attributes/Autodiscovery/StateId.php b/src/Attributes/Autodiscovery/StateId.php index d6453258..e9c5bfa9 100644 --- a/src/Attributes/Autodiscovery/StateId.php +++ b/src/Attributes/Autodiscovery/StateId.php @@ -39,6 +39,12 @@ public function discoverState(Event $event, StateManager $manager): State|array return $manager->make($id, $this->state_type); } + // If we allowed autofill, then we can assume that this creates a new state. + // This prevents having to try to load a snapshot that we know does not exist. + if ($this->autofill && $this->property->getType()->allowsNull()) { + return $manager->make($id, $this->state_type); + } + return collect(Arr::wrap($id)) ->map(fn ($id) => $manager->load($id, $this->state_type)) ->all(); diff --git a/src/Commands/ReplayCommand.php b/src/Commands/ReplayCommand.php index 1c18cf4a..2254e7ec 100644 --- a/src/Commands/ReplayCommand.php +++ b/src/Commands/ReplayCommand.php @@ -3,7 +3,10 @@ namespace Thunk\Verbs\Commands; use Illuminate\Console\Command; +use Illuminate\Database\Events\QueryExecuted; use Illuminate\Support\Facades\App; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Event as EventFacade; use Thunk\Verbs\Contracts\BrokersEvents; use Thunk\Verbs\Event; use Thunk\Verbs\Models\VerbEvent; @@ -25,12 +28,23 @@ public function handle(BrokersEvents $broker): int return 1; } - $progress = progress('Replaying…', VerbEvent::count()); + // Prepare for a long-running, database-heavy run + ini_set('memory_limit', '-1'); + EventFacade::forget(QueryExecuted::class); + DB::disableQueryLog(); + + $started_at = time(); + $progress = progress('Replaying…', VerbEvent::count()); $progress->start(); $broker->replay( - beforeEach: fn (Event $event) => $progress->label(sprintf('%s (%d)', $event::class, $event->id)), + beforeEach: fn (Event $event) => $progress->label(sprintf( + '[%s] %s::%d', + date('i:s', time() - $started_at), + class_basename($event), + $event->id, + )), afterEach: fn () => $progress->advance(), ); diff --git a/src/Lifecycle/Broker.php b/src/Lifecycle/Broker.php index 65734d1e..aba8630a 100644 --- a/src/Lifecycle/Broker.php +++ b/src/Lifecycle/Broker.php @@ -82,8 +82,10 @@ public function replay(?callable $beforeEach = null, ?callable $afterEach = null try { $this->states->reset(include_storage: true); + $iteration = 0; + app(StoresEvents::class)->read() - ->each(function (Event $event) use ($beforeEach, $afterEach) { + ->each(function (Event $event) use ($beforeEach, $afterEach, &$iteration) { $this->states->setReplaying(true); if ($beforeEach) { @@ -97,10 +99,14 @@ public function replay(?callable $beforeEach = null, ?callable $afterEach = null $afterEach($event); } - $this->states->writeSnapshots(); - $this->states->prune(); + if ($iteration++ % 500 === 0) { + $this->states->writeSnapshots(); + $this->states->prune(); + } }); } finally { + $this->states->writeSnapshots(); + $this->states->prune(); $this->states->setReplaying(false); $this->is_replaying = false; } From 9acaa61cd85e260ef038a7113338e89a049f9ead Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 23 Aug 2024 21:24:36 -0400 Subject: [PATCH 05/16] Better newly-created state handling --- src/Lifecycle/StateManager.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Lifecycle/StateManager.php b/src/Lifecycle/StateManager.php index f52da456..4ea4974e 100644 --- a/src/Lifecycle/StateManager.php +++ b/src/Lifecycle/StateManager.php @@ -3,6 +3,7 @@ namespace Thunk\Verbs\Lifecycle; use Glhd\Bits\Bits; +use LogicException; use Ramsey\Uuid\UuidInterface; use ReflectionClass; use Symfony\Component\Uid\AbstractUid; @@ -86,9 +87,13 @@ public function singleton(string $type): State */ public function make(Bits|UuidInterface|AbstractUid|int|string $id, string $type): State { + // If we've already instantiated this state, we'll load it + if ($existing = $this->states->get($this->key($id, $type))) { + return $existing; + } + // State::__construct() auto-registers the state with the StateManager, // so we need to skip the constructor until we've already set the ID. - $state = (new ReflectionClass($type))->newInstanceWithoutConstructor(); $state->id = Id::from($id); $state->__construct(); @@ -151,6 +156,14 @@ protected function remember(State $state): State { $key = $this->key($state->id, $state::class); + if ($this->states->get($key) === $state) { + return $state; + } + + if ($this->states->has($key)) { + throw new LogicException('Trying to remember state twice.'); + } + $this->states->put($key, $state); return $state; From c543a209e8a01dc88c6f9eeed6c171b3dfe1d51e Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 23 Aug 2024 21:41:33 -0400 Subject: [PATCH 06/16] Use 'autofilled' metadata --- src/Attributes/Autodiscovery/StateId.php | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Attributes/Autodiscovery/StateId.php b/src/Attributes/Autodiscovery/StateId.php index e9c5bfa9..6366241d 100644 --- a/src/Attributes/Autodiscovery/StateId.php +++ b/src/Attributes/Autodiscovery/StateId.php @@ -26,9 +26,11 @@ public function __construct( public function discoverState(Event $event, StateManager $manager): State|array { $id = $this->property->getValue($event); + $property_name = $this->property->getName(); + $meta = $event->metadata(); if (! is_array($id)) { - $this->alias ??= $this->inferAliasFromVariableName($this->property->getName()); + $this->alias ??= $this->inferAliasFromVariableName($property_name); } // If the ID hasn't been set yet, we'll automatically set one @@ -36,17 +38,19 @@ public function discoverState(Event $event, StateManager $manager): State|array $id = snowflake_id(); $this->property->setValue($event, $id); + $autofilled = $meta->get('autofilled', []); + $autofilled[$property_name] = true; + $meta->put('autofilled', $autofilled); + return $manager->make($id, $this->state_type); } - // If we allowed autofill, then we can assume that this creates a new state. - // This prevents having to try to load a snapshot that we know does not exist. - if ($this->autofill && $this->property->getType()->allowsNull()) { + // If we autofilled the value when it first fired, then we know this is the + // first event for that given state, and we don't need to try to load it + if ($meta->get("autofilled.{$property_name}", false)) { return $manager->make($id, $this->state_type); } - return collect(Arr::wrap($id)) - ->map(fn ($id) => $manager->load($id, $this->state_type)) - ->all(); + return array_map(fn ($id) => $manager->load($id, $this->state_type), Arr::wrap($id)); } } From 255e94394869ce0dc46bff77a2441933a7b767ae Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 23 Aug 2024 21:42:01 -0400 Subject: [PATCH 07/16] wip --- src/Attributes/Projection/EagerLoad.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/Attributes/Projection/EagerLoad.php diff --git a/src/Attributes/Projection/EagerLoad.php b/src/Attributes/Projection/EagerLoad.php new file mode 100644 index 00000000..e69de29b From 1a03c9f05b025ffab04edb62ba42f69804a2fc66 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 23 Aug 2024 22:41:14 -0400 Subject: [PATCH 08/16] wip --- src/Attributes/Projection/EagerLoad.php | 47 +++++++++++++++++ src/Exceptions/AttributeNotAllowed.php | 7 +++ src/Support/EagerLoader.php | 62 ++++++++++++++++++++++ tests/Feature/EagerLoadingTest.php | 68 +++++++++++++++++++++++++ 4 files changed, 184 insertions(+) create mode 100644 src/Exceptions/AttributeNotAllowed.php create mode 100644 src/Support/EagerLoader.php create mode 100644 tests/Feature/EagerLoadingTest.php diff --git a/src/Attributes/Projection/EagerLoad.php b/src/Attributes/Projection/EagerLoad.php index e69de29b..4f51fc09 100644 --- a/src/Attributes/Projection/EagerLoad.php +++ b/src/Attributes/Projection/EagerLoad.php @@ -0,0 +1,47 @@ +isPublic() || $property->isStatic()) { + throw new AttributeNotAllowed('You can only eager-load protected instance properties.'); + } + + $type = $property->getType(); + if (! $type instanceof ReflectionNamedType) { + throw new AttributeNotAllowed('You can only apply #[EagerLoad] to properties with a type hint.'); + } + + $class_name = $type->getName(); + if (! is_a($class_name, Model::class, true)) { + throw new AttributeNotAllowed('You can only eager load eloquent models.'); + } + + $name = $property->getName(); + $this->id_attribute ??= Str::snake($name).'_id'; + + if (! property_exists($event, $this->id_attribute)) { + $event_class = class_basename($event); + throw new InvalidArgumentException("Unable to find property '{$this->id_attribute}' on '{$event_class}'."); + } + + return [$class_name, $event, $this->id_attribute, $name]; + } +} diff --git a/src/Exceptions/AttributeNotAllowed.php b/src/Exceptions/AttributeNotAllowed.php new file mode 100644 index 00000000..11fb370b --- /dev/null +++ b/src/Exceptions/AttributeNotAllowed.php @@ -0,0 +1,7 @@ +events) + ->map($this->discover(...)) + ->reduce(function (array $map, Collection $discovered) { + foreach ($discovered as [$class_name, $event, $id_property, $target_property]) { + $map['load'][$class_name][] = $event->{$id_property}; + $map['fill'][$class_name][$event->{$id_property}][] = [$event, $target_property]; + } + + return $map; + }, ['load' => [], 'fill' => []]); + + /** @var class-string $class_name */ + foreach ($discovered['load'] as $class_name => $keys) { + $class_name::query() + ->whereIn((new $class_name)->getKeyName(), $keys) + ->eachById(function (Model $model) use ($discovered) { + foreach ($discovered['fill'][$model::class][$model->getKey()] as [$event, $target_property]) { + // This let's us set the property even if it's protected + (fn () => $this->{$target_property} = clone $model)(...)->call($event); + } + }); + } + } + + protected function discover(Event $event): Collection + { + return collect((new ReflectionClass($event))->getProperties()) + ->map(function (ReflectionProperty $property) use ($event) { + $attribute = Arr::first($property->getAttributes(EagerLoad::class)); + + return $attribute?->newInstance()->handle($property, $event); + }) + ->filter() + ->values(); + } +} diff --git a/tests/Feature/EagerLoadingTest.php b/tests/Feature/EagerLoadingTest.php new file mode 100644 index 00000000..8b810d88 --- /dev/null +++ b/tests/Feature/EagerLoadingTest.php @@ -0,0 +1,68 @@ +getProperties()) + ->map(function (ReflectionProperty $property) use ($event) { + $attribute = Arr::first($property->getAttributes(EagerLoad::class)); + + return $attribute?->newInstance()->handle($property, $event); + }) + ->filter() + ->values(); + + expect($attrs->all())->toBe([[TestEagerLoadingModel::class, 1337]]); +}); + +it('eager-loads models for events', function () { + TestEagerLoadingModel::migrate(); + + $model1 = TestEagerLoadingModel::create(['id' => 1337, 'name' => 'test 1']); + $model2 = TestEagerLoadingModel::create(['id' => 9876, 'name' => 'test 2']); + + $event1 = new TestEagerLoadingEvent(1337); + $event2 = new TestEagerLoadingEvent(9876); + + EagerLoader::load($event1, $event2); + + expect($event1->getTestModel()->toArray())->toBe($model1->toArray()) + ->and($event2->getTestModel()->toArray())->toBe($model2->toArray()); +}); + +class TestEagerLoadingEvent extends Event +{ + public function __construct( + public int $test_model_id, + ) {} + + #[EagerLoad] + protected ?TestEagerLoadingModel $test_model = null; + + public function getTestModel(): ?TestEagerLoadingModel + { + return $this->test_model; + } +} + +class TestEagerLoadingModel extends Model +{ + public $timestamps = false; + + protected $table = 'test_eager_loading'; + + public static function migrate() + { + Schema::create('test_eager_loading', function (Blueprint $table) { + $table->snowflakeId(); + $table->string('name')->nullable(); + }); + } +} From 597f4edfd62e415d4ac4cd6c8c4191b506c12b23 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 23 Aug 2024 22:44:54 -0400 Subject: [PATCH 09/16] Use eager load in broker --- src/Lifecycle/Broker.php | 37 +++++++++++++++++------------- tests/Feature/EagerLoadingTest.php | 2 +- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/Lifecycle/Broker.php b/src/Lifecycle/Broker.php index aba8630a..43b4476e 100644 --- a/src/Lifecycle/Broker.php +++ b/src/Lifecycle/Broker.php @@ -2,12 +2,14 @@ namespace Thunk\Verbs\Lifecycle; +use Illuminate\Support\Enumerable; use Thunk\Verbs\CommitsImmediately; use Thunk\Verbs\Contracts\BrokersEvents; use Thunk\Verbs\Contracts\StoresEvents; use Thunk\Verbs\Event; use Thunk\Verbs\Exceptions\EventNotValid; use Thunk\Verbs\Lifecycle\Queue as EventQueue; +use Thunk\Verbs\Support\EagerLoader; class Broker implements BrokersEvents { @@ -68,6 +70,8 @@ public function commit(): bool $this->states->writeSnapshots(); $this->states->prune(); + EagerLoader::load(...$events); + foreach ($events as $event) { $this->metadata->setLastResults($event, $this->dispatcher->handle($event)); } @@ -82,27 +86,28 @@ public function replay(?callable $beforeEach = null, ?callable $afterEach = null try { $this->states->reset(include_storage: true); - $iteration = 0; - app(StoresEvents::class)->read() - ->each(function (Event $event) use ($beforeEach, $afterEach, &$iteration) { - $this->states->setReplaying(true); + ->chunk(500) + ->each(function (Enumerable $events) use ($beforeEach, $afterEach) { + EagerLoader::load(...$events); + + $events->each(function (Event $event) use ($beforeEach, $afterEach) { + $this->states->setReplaying(true); - if ($beforeEach) { - $beforeEach($event); - } + if ($beforeEach) { + $beforeEach($event); + } - $this->dispatcher->apply($event); - $this->dispatcher->replay($event); + $this->dispatcher->apply($event); + $this->dispatcher->replay($event); - if ($afterEach) { - $afterEach($event); - } + if ($afterEach) { + $afterEach($event); + } + }); - if ($iteration++ % 500 === 0) { - $this->states->writeSnapshots(); - $this->states->prune(); - } + $this->states->writeSnapshots(); + $this->states->prune(); }); } finally { $this->states->writeSnapshots(); diff --git a/tests/Feature/EagerLoadingTest.php b/tests/Feature/EagerLoadingTest.php index 8b810d88..d111a0a8 100644 --- a/tests/Feature/EagerLoadingTest.php +++ b/tests/Feature/EagerLoadingTest.php @@ -19,7 +19,7 @@ ->filter() ->values(); - expect($attrs->all())->toBe([[TestEagerLoadingModel::class, 1337]]); + expect($attrs->all())->toBe([[TestEagerLoadingModel::class, $event, 'test_model_id', 'test_model']]); }); it('eager-loads models for events', function () { From e097cc637988b6541b57b92af2359e416eed947f Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Sun, 25 Aug 2024 15:12:55 -0400 Subject: [PATCH 10/16] Chunk snapshots --- src/Lifecycle/SnapshotStore.php | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Lifecycle/SnapshotStore.php b/src/Lifecycle/SnapshotStore.php index d409a097..e1a301f5 100644 --- a/src/Lifecycle/SnapshotStore.php +++ b/src/Lifecycle/SnapshotStore.php @@ -49,11 +49,19 @@ public function write(array $states): bool return true; } - return VerbSnapshot::upsert( - values: collect($states)->map($this->formatForWrite(...))->unique('id')->all(), - uniqueBy: ['id'], - update: ['data', 'last_event_id', 'updated_at'] - ); + foreach (array_chunk($states, 20) as $chunk) { + $upserted = VerbSnapshot::upsert( + values: collect($chunk)->map($this->formatForWrite(...))->unique('id')->all(), + uniqueBy: ['id'], + update: ['data', 'last_event_id', 'updated_at'] + ); + + if (! $upserted) { + return false; + } + } + + return true; } public function reset(): bool From 66d9e825da83c6e263976d13903e3784aed1c380 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Sun, 25 Aug 2024 15:17:17 -0400 Subject: [PATCH 11/16] =?UTF-8?q?Whoops=E2=80=A6=20revert=20change=20from?= =?UTF-8?q?=20merge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Lifecycle/Broker.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Lifecycle/Broker.php b/src/Lifecycle/Broker.php index eb456582..43b4476e 100644 --- a/src/Lifecycle/Broker.php +++ b/src/Lifecycle/Broker.php @@ -86,14 +86,12 @@ public function replay(?callable $beforeEach = null, ?callable $afterEach = null try { $this->states->reset(include_storage: true); - $iteration = 0; - app(StoresEvents::class)->read() ->chunk(500) ->each(function (Enumerable $events) use ($beforeEach, $afterEach) { EagerLoader::load(...$events); - $events->each(function (Event $event) use ($beforeEach, $afterEach, &$iteration) { + $events->each(function (Event $event) use ($beforeEach, $afterEach) { $this->states->setReplaying(true); if ($beforeEach) { @@ -108,10 +106,8 @@ public function replay(?callable $beforeEach = null, ?callable $afterEach = null } }); - if ($iteration++ % 500 === 0) { - $this->states->writeSnapshots(); - $this->states->prune(); - } + $this->states->writeSnapshots(); + $this->states->prune(); }); } finally { $this->states->writeSnapshots(); From fe91a01f2a71404d7dd68f234b1d6065e02dd075 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Thu, 19 Dec 2024 13:16:04 -0500 Subject: [PATCH 12/16] Don't clone --- src/Support/EagerLoader.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Support/EagerLoader.php b/src/Support/EagerLoader.php index bca91529..335e56d5 100644 --- a/src/Support/EagerLoader.php +++ b/src/Support/EagerLoader.php @@ -41,8 +41,8 @@ public function __invoke() ->whereIn((new $class_name)->getKeyName(), $keys) ->eachById(function (Model $model) use ($discovered) { foreach ($discovered['fill'][$model::class][$model->getKey()] as [$event, $target_property]) { - // This let's us set the property even if it's protected - (fn () => $this->{$target_property} = clone $model)(...)->call($event); + // This lets us set the property even if it's protected + (fn () => $this->{$target_property} = $model)(...)->call($event); } }); } From 63db852ec257c384be08cc921368ec227ddcf897 Mon Sep 17 00:00:00 2001 From: inxilpro Date: Thu, 19 Dec 2024 18:18:41 +0000 Subject: [PATCH 13/16] Fix styling --- tests/Unit/UseStatesDirectlyInEventsTest.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/Unit/UseStatesDirectlyInEventsTest.php b/tests/Unit/UseStatesDirectlyInEventsTest.php index b1c6a5ed..dc127e43 100644 --- a/tests/Unit/UseStatesDirectlyInEventsTest.php +++ b/tests/Unit/UseStatesDirectlyInEventsTest.php @@ -101,7 +101,7 @@ $this->assertEquals($event2->id, $user_request2->last_event_id); }); -it('supports union typed properties in events', function() { +it('supports union typed properties in events', function () { $user_request = UserRequestState::new(); UserRequestsWithUnionTypes::commit( @@ -169,14 +169,15 @@ public function apply() } } -class UserRequestsWithUnionTypes extends Event +class UserRequestsWithUnionTypes extends Event { public function __construct( public UserRequestState $user_request, public string|int $value ) {} - public function apply() { + public function apply() + { $this->user_request->unionTypedValue = $this->value; } } From 11a1b471122107ee9ebf86217b213f238ca1fa6b Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Thu, 19 Dec 2024 13:25:55 -0500 Subject: [PATCH 14/16] Don't hard-code IDs --- tests/Feature/EagerLoadingTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Feature/EagerLoadingTest.php b/tests/Feature/EagerLoadingTest.php index d111a0a8..12369b1c 100644 --- a/tests/Feature/EagerLoadingTest.php +++ b/tests/Feature/EagerLoadingTest.php @@ -25,16 +25,16 @@ it('eager-loads models for events', function () { TestEagerLoadingModel::migrate(); - $model1 = TestEagerLoadingModel::create(['id' => 1337, 'name' => 'test 1']); - $model2 = TestEagerLoadingModel::create(['id' => 9876, 'name' => 'test 2']); + $model1 = TestEagerLoadingModel::create(['name' => 'test 1']); + $model2 = TestEagerLoadingModel::create(['name' => 'test 2']); - $event1 = new TestEagerLoadingEvent(1337); - $event2 = new TestEagerLoadingEvent(9876); + $event1 = new TestEagerLoadingEvent($model1->getKey()); + $event2 = new TestEagerLoadingEvent($model2->getKey()); EagerLoader::load($event1, $event2); - expect($event1->getTestModel()->toArray())->toBe($model1->toArray()) - ->and($event2->getTestModel()->toArray())->toBe($model2->toArray()); + expect($model1->is($event1->getTestModel()))->toBeTrue() + ->and($model2->is($event2->getTestModel()))->toBeTrue(); }); class TestEagerLoadingEvent extends Event From f3f6d65f98815e8e928fa71c88eebc7a68f5c236 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Thu, 19 Dec 2024 13:27:40 -0500 Subject: [PATCH 15/16] hm --- tests/Feature/EagerLoadingTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Feature/EagerLoadingTest.php b/tests/Feature/EagerLoadingTest.php index 12369b1c..c60b05fa 100644 --- a/tests/Feature/EagerLoadingTest.php +++ b/tests/Feature/EagerLoadingTest.php @@ -25,11 +25,11 @@ it('eager-loads models for events', function () { TestEagerLoadingModel::migrate(); - $model1 = TestEagerLoadingModel::create(['name' => 'test 1']); - $model2 = TestEagerLoadingModel::create(['name' => 'test 2']); + $model1 = TestEagerLoadingModel::create(['id' => 1337, 'name' => 'test 1']); + $model2 = TestEagerLoadingModel::create(['id' => 9876, 'name' => 'test 2']); - $event1 = new TestEagerLoadingEvent($model1->getKey()); - $event2 = new TestEagerLoadingEvent($model2->getKey()); + $event1 = new TestEagerLoadingEvent(1337); + $event2 = new TestEagerLoadingEvent(9876); EagerLoader::load($event1, $event2); From 95433f9daca279446748a52a9e7c8da51a7a57b6 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Thu, 19 Dec 2024 13:35:10 -0500 Subject: [PATCH 16/16] Fix $incrementing --- tests/Feature/EagerLoadingTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Feature/EagerLoadingTest.php b/tests/Feature/EagerLoadingTest.php index c60b05fa..59dc78ca 100644 --- a/tests/Feature/EagerLoadingTest.php +++ b/tests/Feature/EagerLoadingTest.php @@ -54,6 +54,8 @@ public function getTestModel(): ?TestEagerLoadingModel class TestEagerLoadingModel extends Model { + public $incrementing = false; + public $timestamps = false; protected $table = 'test_eager_loading';