Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
cf7d914
refactor(events): extract Event root (name/prefix) above DomainEvent
titustangible Jul 6, 2026
235cce8
feat(events): RecordBehaviour trait — strict scalarise codec, journey…
titustangible Jul 6, 2026
9822cfd
refactor(events): rename RecordBehaviour -> IntegrationBehaviour (own…
titustangible Jul 6, 2026
ee75059
chore: gitignore .superpowers scratch
titustangible Jul 6, 2026
fa6d56f
feat(events)!: sever the partition — IntegrationEvent is a derived-on…
titustangible Jul 6, 2026
03e0f0f
test(integration): update outbox suite event stubs to severed IIntegr…
titustangible Jul 6, 2026
d3b5dbd
feat(events): single-door router (IAnnouncesIntegration) + AlreadyInt…
titustangible Jul 6, 2026
c23cec4
feat(events): TransportEnvelope — single unwrap point for journey tra…
titustangible Jul 6, 2026
4866572
feat(listeners): IntegrationListener + integration_listener() ceremon…
titustangible Jul 6, 2026
d5b4841
feat(process): IAwaitMechanism strategy — AwaitEvent refactor, Result…
titustangible Jul 6, 2026
99517d9
feat(process): AwaitAll fan-in mechanism — key_by static extractor, m…
titustangible Jul 6, 2026
1747cb6
feat(process)!: mechanism-driven resume — accumulate-until-satisfied,…
titustangible Jul 6, 2026
9254198
fix(process): stop run() relabelling AwaitedEventNotRegistered as a s…
titustangible Jul 6, 2026
c3f581e
feat(process): wall-clock await timeout (stale guard, fail|proceed po…
titustangible Jul 6, 2026
40bdde3
fix(process): handle_timeout arms resource governor + serializes via …
titustangible Jul 6, 2026
1db720e
test(pipeline): end-to-end pipe — raise → outbox → AS → typed wake; C…
titustangible Jul 6, 2026
6b10736
fix(pipeline-test): assert correlation VALUES survive the hop, not ju…
titustangible Jul 6, 2026
5b825fb
fix(schema): migrate v4 — await_mechanism column for upgraded installs
titustangible Jul 6, 2026
ba539a6
chore(deps): sync composer.lock with require-dev additions
titustangible Jul 10, 2026
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
4 changes: 2 additions & 2 deletions .github/workflows/phpunit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: PHPUnit tests

on:
push:
branches: [ master ]
branches: [ master, v3-ddd, 'release/**' ]
pull_request:
branches: [ master ]
branches: [ master, v3-ddd, 'release/**' ]

jobs:
test:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ htmlcov/
# Claude session scratch
.claude/worktrees/
.playwright-cli/
.superpowers/
189 changes: 188 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@
namespace TangibleDDD\Application\EventHandlers;

/**
* @deprecated 0.2.0 — the "async domain handler" is a category error (the AS hop
* is another TIME, not another thread; serialization forces params into
* record-land regardless of intent). Decompose into an IntegrationListener
* (the policy) + a Command (the work, under command_audit). Removal: 0.3.0.
*
* Event handler that automatically queues the actual handling
* to run asynchronously via Action Scheduler.
*
*
* When the domain event fires, it enqueues an async action.
* The actual handle() method runs in a separate request.
*/
Expand Down
32 changes: 32 additions & 0 deletions ddd-src/Application/EventHandlers/IntegrationListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace TangibleDDD\Application\EventHandlers;

use TangibleDDD\Application\Commands\ICommand;
use TangibleDDD\Domain\Events\IIntegrationEvent;

/**
* A stateless automation policy: "whenever [fact], then [intention]."
*
* The whole job is get_command() — fact in, intention out, null = not my
* business. All work belongs in the command's handler (audit, retry,
* causation); a listener only translates. Auto-wired by namespace convention
* \Application\IntegrationListeners\ (eager boot constructs via the container,
* so ctor injection is available to subclasses that need it — the happy path
* needs nothing).
*/
abstract class IntegrationListener {

/** @return class-string<IIntegrationEvent> */
abstract protected function get_event_class(): string;

/** Fact in, intention out. Null = no reaction. */
abstract protected function get_command(IIntegrationEvent $event): ?ICommand;

public function __construct() {
\TangibleDDD\WordPress\integration_listener(
static::get_event_class(),
fn(IIntegrationEvent $event) => $this->get_command($event)
);
}
}
10 changes: 5 additions & 5 deletions ddd-src/Application/Events/EventRouter.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

namespace TangibleDDD\Application\Events;

use TangibleDDD\Domain\Events\IAnnouncesIntegration;
use TangibleDDD\Domain\Events\IDomainEvent;
use TangibleDDD\Domain\Events\IIntegrationEvent;

/**
* Routes domain events to appropriate destinations.
*
* 1. All domain events go to the dispatcher (WordPress hooks)
* 2. Integration events additionally go to the bus (outbox)
* 1. All domain events go to the dispatcher (WordPress hooks).
* 2. Announcing events additionally send their record to the bus.
*/
final class EventRouter {
public function __construct(
Expand All @@ -20,8 +20,8 @@ public function __construct(
public function publish(IDomainEvent $event): void {
$this->dispatcher->dispatch($event);

if ($event instanceof IIntegrationEvent) {
$this->bus->publish($event);
if ($event instanceof IAnnouncesIntegration) {
$this->bus->publish($event->to_integration());
}
}
}
4 changes: 4 additions & 0 deletions ddd-src/Application/Events/EventsUnitOfWork.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace TangibleDDD\Application\Events;

use TangibleDDD\Application\Exceptions\DomainEventAfterSealException;
use TangibleDDD\Domain\Events\AlreadyIntegrated;
use TangibleDDD\Domain\Events\IDomainEvent;
use TangibleDDD\Domain\Events\IIntegrationEvent;
use TangibleDDD\Domain\Shared\IRecordsDomainEvents;
Expand Down Expand Up @@ -41,6 +42,9 @@ public function seal(): void {
}

public function record(IDomainEvent $event): void {
if ($event instanceof IIntegrationEvent && $event->event_id() !== null) {
throw new AlreadyIntegrated(get_class($event), $event->event_id());
}
if ($this->sealed && !$event instanceof IIntegrationEvent) {
throw new DomainEventAfterSealException(get_class($event));
}
Expand Down
43 changes: 43 additions & 0 deletions ddd-src/Application/Events/TransportEnvelope.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace TangibleDDD\Application\Events;

use TangibleDDD\Application\Correlation\CorrelationContext;

/**
* The journey's wire form. OutboxProcessor smears __-keys into the payload bag
* for transport; this is the single place they are separated back out.
* Used by the WP integration_action() helper, the listener ceremony, and the
* process runner's wake path.
*/
final class TransportEnvelope {

private function __construct(
public readonly array $payload,
public readonly ?string $correlation_id,
public readonly ?int $sequence,
public readonly ?string $event_id,
) {}

public static function unwrap(array $wrapped): self {
$correlation_id = isset($wrapped['__correlation_id']) ? (string) $wrapped['__correlation_id'] : null;
$sequence = isset($wrapped['__sequence']) ? (int) $wrapped['__sequence'] : null;
$event_id = isset($wrapped['__event_id']) ? (string) $wrapped['__event_id'] : null;
unset($wrapped['__correlation_id'], $wrapped['__sequence'], $wrapped['__event_id']);

return new self($wrapped, $correlation_id, $sequence, $event_id);
}

/** Restore ambient correlation + stash this event as causation for dispatched commands. */
public function restore_context(): void {
if ($this->correlation_id !== null) {
CorrelationContext::init($this->correlation_id);
}
if ($this->sequence !== null) {
CorrelationContext::set_sequence($this->sequence);
}
if ($this->event_id !== null) {
CorrelationContext::set_causation($this->event_id, 'integration_event');
}
}
}
Loading
Loading