You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by da...@apache.org on 2023/03/03 18:38:38 UTC

[causeway] branch CAUSEWAY-3366 updated (5c7264a1eb -> 356bf204b6)

This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a change to branch CAUSEWAY-3366
in repository https://gitbox.apache.org/repos/asf/causeway.git


    from 5c7264a1eb CAUSEWAY-3366: adds attribute support for pdfjsviewer
     add 301c4d5be5 Merge pull request #1473 from apache/CAUSEWAY-3366
     add 08d13b9133 CAUSEWAY-3367: removes config prop, always persist commands
     new 356bf204b6 CAUSEWAY-3366: wip, adding in onReady and onStarted callbacks

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../ROOT/pages/2023/2.0.0-RC1/mignotes.adoc        | 14 +++-
 .../causeway/applib/services/command/Command.java  | 52 ++++---------
 .../services/publishing/log/CommandLogger.java     | 18 ++++-
 .../services/publishing/spi/CommandSubscriber.java | 29 +++++++-
 .../core/config/CausewayConfiguration.java         | 24 ------
 .../interaction/session/CausewayInteraction.java   | 19 ++++-
 .../metamodel/execution/InteractionInternal.java   |  3 +
 .../publish/command/CommandPublishingFacet.java    | 17 -----
 .../services/publishing/CommandPublisher.java      | 16 +++-
 .../command/CommandExecutorServiceDefault.java     |  8 +-
 .../executor/MemberExecutorServiceDefault.java     | 38 ++++++++--
 .../publish/CommandPublisherDefault.java           | 40 +++++++++-
 .../core/runtimeservices/publish/_Xray.java        | 32 +++++++-
 .../session/InteractionServiceDefault.java         |  4 +-
 .../commandlog/applib/dom/BackgroundService.java   |  1 -
 .../subscriber/CommandSubscriberForCommandLog.java | 87 +++++++++++++++++++---
 .../changetracking/EntityChangeTrackerDefault.java |  1 -
 .../subscriber/CommandSubscriberForTesting.java    | 27 +++++--
 18 files changed, 298 insertions(+), 132 deletions(-)


[causeway] 01/01: CAUSEWAY-3366: wip, adding in onReady and onStarted callbacks

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch CAUSEWAY-3366
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 356bf204b632a6320601c4de021d4663c25a1371
Author: danhaywood <da...@haywood-associates.co.uk>
AuthorDate: Fri Mar 3 18:22:56 2023 +0000

    CAUSEWAY-3366: wip, adding in onReady and onStarted callbacks
---
 .../causeway/applib/services/command/Command.java  | 16 ++++-
 .../services/publishing/log/CommandLogger.java     | 17 ++++-
 .../services/publishing/spi/CommandSubscriber.java | 29 ++++++--
 .../interaction/session/CausewayInteraction.java   | 19 +++--
 .../metamodel/execution/InteractionInternal.java   |  3 +
 .../publish/command/CommandPublishingFacet.java    | 17 -----
 .../services/publishing/CommandPublisher.java      | 16 ++++-
 .../command/CommandExecutorServiceDefault.java     |  8 +--
 .../executor/MemberExecutorServiceDefault.java     | 38 ++++++++--
 .../publish/CommandPublisherDefault.java           | 40 ++++++++++-
 .../core/runtimeservices/publish/_Xray.java        | 32 +++++++--
 .../session/InteractionServiceDefault.java         |  4 +-
 .../commandlog/applib/dom/BackgroundService.java   |  1 -
 .../subscriber/CommandSubscriberForCommandLog.java | 81 +++++++++++++++++++++-
 .../subscriber/CommandSubscriberForTesting.java    | 27 ++++++--
 15 files changed, 288 insertions(+), 60 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/command/Command.java b/api/applib/src/main/java/org/apache/causeway/applib/services/command/Command.java
index f03d85f9ca..cab8bbd14a 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/services/command/Command.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/services/command/Command.java
@@ -236,18 +236,28 @@ public class Command implements HasInteractionId, HasUsername, HasCommandDto {
     public static enum CommandPublishingPhase {
         /** initial state: do not publish (yet) */
         ONHOLD,
-        /** publishing is enabled */
+        /**
+         * publishing is enabled, and the command will be executed.
+         */
         READY,
-        /** publishing has completed */
+        /**
+         * The command has started to be executed.
+         */
+        STARTED,
+        /**
+         * The command has completed its execution.
+         */
         COMPLETED;
         public boolean isOnhold() {return this==ONHOLD;}
         public boolean isReady() {return this==READY;}
+        public boolean isStarted() {return this==STARTED;}
         public boolean isCompleted() {return this==COMPLETED;}
     }
 
     /**
      * Whether this command has been enabled for publishing,
-     * that is {@link CommandSubscriber}s will be notified when this Command completes.
+     * that is {@link CommandSubscriber}s will be notified when this Command becomes {@link CommandPublishingPhase#READY ready},
+     * has {@link CommandPublishingPhase#STARTED started}, and when it {@link CommandPublishingPhase#COMPLETED completes}.
      */
     @Getter private CommandPublishingPhase publishingPhase = CommandPublishingPhase.ONHOLD;
 
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/publishing/log/CommandLogger.java b/api/applib/src/main/java/org/apache/causeway/applib/services/publishing/log/CommandLogger.java
index ce062b3c73..3760ca8431 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/services/publishing/log/CommandLogger.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/services/publishing/log/CommandLogger.java
@@ -53,13 +53,28 @@ public class CommandLogger implements CommandSubscriber {
         return log.isDebugEnabled();
     }
 
+    @Override
+    public void onReady(Command command) {
+        on("ready", command);
+    }
+
+    @Override
+    public void onStarted(Command command) {
+        on("started", command);
+    }
+
     @Override
     public void onCompleted(final Command command) {
 
+        on("completed", command);
+    }
+
+    private static void on(String verb, Command command) {
         val commandDto = command.getCommandDto();
         val xml = CommandDtoUtils.dtoMapper().toString(commandDto);
 
-        log.debug("completed: {} \n{}",
+        log.debug("{}: {} \n{}",
+                verb,
                 command.getLogicalMemberIdentifier(),
                 xml);
     }
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/publishing/spi/CommandSubscriber.java b/api/applib/src/main/java/org/apache/causeway/applib/services/publishing/spi/CommandSubscriber.java
index b412dbcc38..f4ca48fd65 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/services/publishing/spi/CommandSubscriber.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/services/publishing/spi/CommandSubscriber.java
@@ -22,19 +22,40 @@ import org.apache.causeway.applib.services.command.Command;
 import org.apache.causeway.commons.having.HasEnabling;
 
 /**
- * Part of the <i>Publishing SPI</i>. A component to receive {@link Command}s 
+ * Part of the <i>Publishing SPI</i>. A component to receive {@link Command}s
  * (with publishing enabled) that just completed.
- *  
+ *
  * @since 2.0 {@index}
  */
 public interface CommandSubscriber extends HasEnabling {
 
     /**
-     * Notifies that the command has completed.
+     * Notifies that the command will be published, and has transitioned to {@link org.apache.causeway.applib.services.command.Command.CommandPublishingPhase#READY}.
+     *
+     * <p>
+     *     This is an opportunity for implementations to process the command,
+     *     for example to persist an initial representation of it.
+     * </p>
+     */
+    void onReady(Command command);
+
+    /**
+     * Notifies that the command has started to execute, and has transitioned to {@link org.apache.causeway.applib.services.command.Command.CommandPublishingPhase#STARTED}.
+     *
+     * <p>
+     *     This is an opportunity for implementations to process the command,
+     *     for example to update any persisted representation of it.
+     * </p>
+     */
+    void onStarted(Command command);
+
+
+    /**
+     * Notifies that the command has completed and has transitioned to {@link org.apache.causeway.applib.services.command.Command.CommandPublishingPhase#COMPLETED}
      *
      * <p>
      *     This is an opportunity for implementations to process the command,
-     *     for example to persist a representation of it.
+     *     for example to update any persisted representations of it.
      * </p>
      */
     void onCompleted(Command command);
diff --git a/core/interaction/src/main/java/org/apache/causeway/core/interaction/session/CausewayInteraction.java b/core/interaction/src/main/java/org/apache/causeway/core/interaction/session/CausewayInteraction.java
index 9d3b12aa28..c8fc97a9f0 100644
--- a/core/interaction/src/main/java/org/apache/causeway/core/interaction/session/CausewayInteraction.java
+++ b/core/interaction/src/main/java/org/apache/causeway/core/interaction/session/CausewayInteraction.java
@@ -36,6 +36,7 @@ import org.apache.causeway.commons.internal.base._Casts;
 import org.apache.causeway.commons.internal.collections._Lists;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
 import org.apache.causeway.core.metamodel.execution.InteractionInternal;
+import org.apache.causeway.core.metamodel.services.publishing.CommandPublisher;
 
 import lombok.Getter;
 import lombok.NonNull;
@@ -99,9 +100,10 @@ implements InteractionInternal {
             final ActionInvocation actionInvocation,
             final ClockService clockService,
             final MetricsService metricsService,
+            final CommandPublisher commandPublisher,
             final Command command) {
 
-        pushAndStart(actionInvocation, clockService, metricsService, command);
+        pushAndStart(actionInvocation, clockService, metricsService, commandPublisher, command);
         try {
             return executeInternal(memberExecutor, actionInvocation);
         } finally {
@@ -109,9 +111,14 @@ implements InteractionInternal {
         }
     }
 
-    private void pushAndStart(final ActionInvocation actionInvocation, final ClockService clockService, final MetricsService metricsService, final Command command) {
+    private void pushAndStart(
+            final ActionInvocation actionInvocation,
+            final ClockService clockService,
+            final MetricsService metricsService,
+            final CommandPublisher commandPublisher,
+            final Command command) {
         push(actionInvocation);
-        start(actionInvocation, clockService, metricsService, command);
+        start(actionInvocation, clockService, metricsService, commandPublisher, command);
     }
 
     @Override
@@ -120,10 +127,11 @@ implements InteractionInternal {
             final PropertyEdit propertyEdit,
             final ClockService clockService,
             final MetricsService metricsService,
+            final CommandPublisher commandPublisher,
             final Command command) {
 
         push(propertyEdit);
-        start(propertyEdit, clockService, metricsService, command);
+        start(propertyEdit, clockService, metricsService, commandPublisher, command);
         try {
             return executeInternal(memberExecutor, propertyEdit);
         } finally {
@@ -186,6 +194,7 @@ implements InteractionInternal {
             final Execution<?,?> execution,
             final ClockService clockService,
             final MetricsService metricsService,
+            final CommandPublisher commandPublisher,
             final Command command) {
         // set the startedAt (and update command if this is the top-most member execution)
         // (this isn't done within Interaction#execute(...) because it requires the DTO
@@ -193,7 +202,9 @@ implements InteractionInternal {
         val startedAt = execution.start(clockService, metricsService);
         if(command.getStartedAt() == null) {
             command.updater().setStartedAt(startedAt);
+            command.updater().setPublishingPhase(Command.CommandPublishingPhase.STARTED);
         }
+        commandPublisher.start(command);
     }
 
     /**
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/execution/InteractionInternal.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/execution/InteractionInternal.java
index d10a6f860f..bf67058c81 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/execution/InteractionInternal.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/execution/InteractionInternal.java
@@ -29,6 +29,7 @@ import org.apache.causeway.applib.services.iactn.Interaction;
 import org.apache.causeway.applib.services.iactn.PropertyEdit;
 import org.apache.causeway.applib.services.metrics.MetricsService;
 import org.apache.causeway.applib.services.wrapper.WrapperFactory;
+import org.apache.causeway.core.metamodel.services.publishing.CommandPublisher;
 
 import lombok.NonNull;
 import lombok.val;
@@ -61,6 +62,7 @@ extends Interaction {
             final ActionInvocation actionInvocation,
             final ClockService clockService,
             final MetricsService metricsService,
+            final CommandPublisher commandPublisher,
             final Command command);
 
     /**
@@ -77,6 +79,7 @@ extends Interaction {
             final PropertyEdit propertyEdit,
             final ClockService clockService,
             final MetricsService metricsService,
+            final CommandPublisher commandPublisher,
             final Command command);
 
 
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacet.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacet.java
index dfaf4c2ce1..58d58db465 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacet.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacet.java
@@ -49,21 +49,4 @@ public interface CommandPublishingFacet extends Facet {
     public static boolean isPublishingEnabled(final @NonNull FacetHolder facetHolder) {
         return facetHolder.containsFacet(CommandPublishingFacet.class);
     }
-
-    /**
-     * Will set the command's CommandPublishingPhase to READY,
-     * if command and objectMember have a matching member-id
-     * and if the facetHoler has a CommandPublishingFacet (has commandPublishing=ENABLED).
-     */
-    public static void prepareCommandForPublishing(
-            final @NonNull Command command,
-            final @NonNull InteractionHead interactionHead,
-            final @NonNull ObjectMember objectMember,
-            final @NonNull FacetHolder facetHolder) {
-
-        if(IdentifierUtil.isCommandForMember(command, interactionHead, objectMember)
-                && isPublishingEnabled(facetHolder)) {
-            command.updater().setPublishingPhase(CommandPublishingPhase.READY);
-        }
-    }
 }
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/publishing/CommandPublisher.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/publishing/CommandPublisher.java
index d8fd9e2bf8..cecd04379c 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/publishing/CommandPublisher.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/publishing/CommandPublisher.java
@@ -30,8 +30,20 @@ import lombok.NonNull;
 public interface CommandPublisher {
 
     /**
-     * &quot;Completes&quot; the command, meaning that all {@link CommandSubscriber}s
-     * are notified throuhg {@link CommandSubscriber#onCompleted(Command)}.
+     * Notifies all {@link CommandSubscriber}s (through {@link CommandSubscriber#onReady(Command)}) that the
+     * {@link Command} has been created/is ready for execution
+     */
+    void ready(@NonNull Command command);
+
+    /**
+     * Notifies all {@link CommandSubscriber}s (through {@link CommandSubscriber#onStarted(Command)}) that the
+     * {@link Command} has started.
+     */
+    void start(@NonNull Command command);
+
+    /**
+     * Notifies all {@link CommandSubscriber}s (through {@link CommandSubscriber#onCompleted(Command)}) that the
+     * {@link Command} has completed.
      */
     void complete(@NonNull Command command);
 
diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/command/CommandExecutorServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/command/CommandExecutorServiceDefault.java
index 115d1f38be..5b64abe069 100644
--- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/command/CommandExecutorServiceDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/command/CommandExecutorServiceDefault.java
@@ -38,7 +38,6 @@ import org.apache.causeway.applib.services.command.Command;
 import org.apache.causeway.applib.services.command.CommandExecutorService;
 import org.apache.causeway.applib.services.command.CommandOutcomeHandler;
 import org.apache.causeway.applib.services.iactnlayer.InteractionLayerTracker;
-import org.apache.causeway.applib.services.iactnlayer.InteractionService;
 import org.apache.causeway.applib.services.sudo.SudoService;
 import org.apache.causeway.applib.services.xactn.TransactionService;
 import org.apache.causeway.applib.util.schema.CommandDtoUtils;
@@ -82,10 +81,9 @@ public class CommandExecutorServiceDefault implements CommandExecutorService {
     @Inject final SudoService sudoService;
     @Inject final ClockService clockService;
     @Inject final TransactionService transactionService;
-    @Inject final InteractionLayerTracker iInteractionLayerTracker;
+    @Inject final InteractionLayerTracker interactionLayerTracker;
     @Inject final SchemaValueMarshaller valueMarshaller;
 
-    @Inject @Getter final InteractionService interactionService;
     @Inject @Getter final SpecificationLoader specificationLoader;
 
     @Override
@@ -123,7 +121,7 @@ public class CommandExecutorServiceDefault implements CommandExecutorService {
             final CommandDto dto,
             final CommandOutcomeHandler commandOutcomeHandler) {
 
-        val interaction = iInteractionLayerTracker.currentInteractionElseFail();
+        val interaction = interactionLayerTracker.currentInteractionElseFail();
         val command = interaction.getCommand();
         if(command.getCommandDto() != dto) {
             command.updater().setCommandDto(dto);
@@ -171,7 +169,7 @@ public class CommandExecutorServiceDefault implements CommandExecutorService {
     private void copyStartedAtFromInteractionExecution(
             final CommandOutcomeHandler commandOutcomeHandler) {
 
-        val interaction = iInteractionLayerTracker.currentInteractionElseFail();
+        val interaction = interactionLayerTracker.currentInteractionElseFail();
         val currentExecution = interaction.getCurrentExecution();
 
         val startedAt = currentExecution != null
diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java
index 30f4cbae10..12e41e74d2 100644
--- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java
@@ -18,7 +18,6 @@
  */
 package org.apache.causeway.core.runtimeservices.executor;
 
-import java.lang.reflect.Method;
 import java.util.Optional;
 
 import javax.annotation.Priority;
@@ -26,6 +25,9 @@ import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Provider;
 
+import org.apache.causeway.core.metamodel.facets.actions.action.invocation.IdentifierUtil;
+import org.apache.causeway.core.metamodel.services.publishing.CommandPublisher;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectMember;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Service;
 
@@ -51,7 +53,6 @@ import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy;
 import org.apache.causeway.core.metamodel.execution.InteractionInternal;
 import org.apache.causeway.core.metamodel.execution.MemberExecutorService;
 import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
-import org.apache.causeway.core.metamodel.facets.members.publish.command.CommandPublishingFacet;
 import org.apache.causeway.core.metamodel.facets.members.publish.execution.ExecutionPublishingFacet;
 import org.apache.causeway.core.metamodel.facets.properties.property.modify.PropertySetterOrClearFacetForDomainEventAbstract.EditingVariant;
 import org.apache.causeway.core.metamodel.interactions.InteractionHead;
@@ -77,6 +78,8 @@ import lombok.SneakyThrows;
 import lombok.val;
 import lombok.extern.log4j.Log4j2;
 
+import static org.apache.causeway.core.metamodel.facets.members.publish.command.CommandPublishingFacet.isPublishingEnabled;
+
 @Service
 @Named(CausewayModuleCoreRuntimeServices.NAMESPACE + ".MemberExecutorServiceDefault")
 @Priority(PriorityPrecedence.EARLY)
@@ -96,6 +99,7 @@ implements MemberExecutorService {
     private final @Getter Provider<ExecutionPublisher> executionPublisherProvider;
     private final @Getter MetamodelEventService metamodelEventService;
     private final @Getter TransactionService transactionService;
+    private final Provider<CommandPublisher> commandPublisherProvider;
 
     private MetricsService metricsService() {
         return metricsServiceProvider.get();
@@ -137,7 +141,7 @@ implements MemberExecutorService {
         val interaction = getInteractionElseFail();
         val command = interaction.getCommand();
 
-        CommandPublishingFacet.prepareCommandForPublishing(command, head, owningAction, facetHolder);
+        prepareCommandForPublishing(command, head, owningAction, facetHolder);
 
         val xrayHandle = _Xray.enterActionInvocation(interactionLayerTracker, interaction, owningAction, head, argumentAdapters);
 
@@ -156,7 +160,7 @@ implements MemberExecutorService {
         val memberExecutor = actionExecutorFactory.createExecutor(owningAction, head, argumentAdapters);
 
         // sets up startedAt and completedAt on the execution, also manages the execution call graph
-        interaction.execute(memberExecutor, actionInvocation, clockService, metricsService(), command);
+        interaction.execute(memberExecutor, actionInvocation, clockService, metricsService(), commandPublisherProvider.get(), command);
 
         // handle any exceptions
         val priorExecution = interaction.getPriorExecutionOrThrowIfAnyException(actionInvocation);
@@ -195,6 +199,7 @@ implements MemberExecutorService {
         return result;
     }
 
+
     @Override
     public ManagedObject setOrClearProperty(
             final @NonNull OneToOneAssociation owningProperty,
@@ -211,7 +216,7 @@ implements MemberExecutorService {
             return head.getTarget();
         }
 
-        CommandPublishingFacet.prepareCommandForPublishing(command, head, owningProperty, facetHolder);
+        prepareCommandForPublishing(command, head, owningProperty, facetHolder);
 
         val xrayHandle = _Xray.enterPropertyEdit(interactionLayerTracker, interaction, owningProperty, head, newValueAdapter);
 
@@ -227,7 +232,7 @@ implements MemberExecutorService {
                         interactionInitiatedBy, editingVariant);
 
         // sets up startedAt and completedAt on the execution, also manages the execution call graph
-        val targetPojo = interaction.execute(executor, propertyEdit, clockService, metricsService(), command);
+        val targetPojo = interaction.execute(executor, propertyEdit, clockService, metricsService(), commandPublisherProvider.get(), command);
 
         // handle any exceptions
         final Execution<?, ?> priorExecution = interaction.getPriorExecution();
@@ -313,4 +318,25 @@ implements MemberExecutorService {
                 : null;
     }
 
+
+    /**
+     * Will set the command's CommandPublishingPhase to READY,
+     * if command and objectMember have a matching member-id
+     * and if the facetHolder has a CommandPublishingFacet (has commandPublishing=ENABLED).
+     */
+    void prepareCommandForPublishing(
+            final @NonNull Command command,
+            final @NonNull InteractionHead interactionHead,
+            final @NonNull ObjectMember objectMember,
+            final @NonNull FacetHolder facetHolder) {
+
+        if(IdentifierUtil.isCommandForMember(command, interactionHead, objectMember)
+                && isPublishingEnabled(facetHolder)) {
+            command.updater().setPublishingPhase(Command.CommandPublishingPhase.READY);
+        }
+
+        commandPublisherProvider.get().ready(command);
+
+    }
+
 }
diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/CommandPublisherDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/CommandPublisherDefault.java
index e56288f116..c3f2836f52 100644
--- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/CommandPublisherDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/CommandPublisherDefault.java
@@ -64,17 +64,53 @@ public class CommandPublisherDefault implements CommandPublisher {
                 .filter(HasEnabling::isEnabled);
     }
 
+    @Override
+    public void ready(@NonNull Command command) {
+
+        val handle = _Xray.enterCommandReadyPublishing(
+                interactionServiceProvider.get(),
+                command,
+                enabledSubscribers,
+                ()->getCannotPublishReason(command));
+
+        if(canPublish(command)) {
+            log.debug("about to PUBLISH command {}: {} to {}", "ready", command, enabledSubscribers);
+            enabledSubscribers.forEach(subscriber -> subscriber.onReady(command));
+            command.updater().setPublishingPhase(CommandPublishingPhase.READY);
+        }
+
+        _Xray.exitPublishing(handle);
+    }
+
+    @Override
+    public void start(@NonNull Command command) {
+
+        val handle = _Xray.enterCommandStartedPublishing(
+                interactionServiceProvider.get(),
+                command,
+                enabledSubscribers,
+                ()->getCannotPublishReason(command));
+
+        if(canPublish(command)) {
+            log.debug("about to PUBLISH command {}: {} to {}", "started", command, enabledSubscribers);
+            enabledSubscribers.forEach(subscriber -> subscriber.onStarted(command));
+            command.updater().setPublishingPhase(CommandPublishingPhase.STARTED);
+        }
+
+        _Xray.exitPublishing(handle);
+    }
+
     @Override
     public void complete(final @NonNull Command command) {
 
-        val handle = _Xray.enterCommandPublishing(
+        val handle = _Xray.enterCommandCompletedPublishing(
                 interactionServiceProvider.get(),
                 command,
                 enabledSubscribers,
                 ()->getCannotPublishReason(command));
 
         if(canPublish(command)) {
-            log.debug("about to PUBLISH command: {} to {}", command, enabledSubscribers);
+            log.debug("about to PUBLISH command {}: {} to {}", "completed", command, enabledSubscribers);
             enabledSubscribers.forEach(subscriber -> subscriber.onCompleted(command));
             command.updater().setPublishingPhase(CommandPublishingPhase.COMPLETED); // one shot
         }
diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/_Xray.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/_Xray.java
index edbd162bc2..a4789bf742 100644
--- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/_Xray.java
+++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/_Xray.java
@@ -47,12 +47,36 @@ final class _Xray {
 
     // -- COMMAND
 
-    static SequenceHandle enterCommandPublishing(
+    static SequenceHandle enterCommandReadyPublishing(
             final @NonNull InteractionLayerTracker iaTracker,
             final @Nullable Command command,
             final @NonNull Can<CommandSubscriber> enabledSubscribers,
             final @NonNull Supplier<String> cannotPublishReasonSupplier) {
 
+        return enterCommandPublishing(iaTracker, command, enabledSubscribers, cannotPublishReasonSupplier, "created");
+    }
+
+    static SequenceHandle enterCommandStartedPublishing(
+            final @NonNull InteractionLayerTracker iaTracker,
+            final @Nullable Command command,
+            final @NonNull Can<CommandSubscriber> enabledSubscribers,
+            final @NonNull Supplier<String> cannotPublishReasonSupplier) {
+
+        return enterCommandPublishing(iaTracker, command, enabledSubscribers, cannotPublishReasonSupplier, "started");
+
+    }
+
+    static SequenceHandle enterCommandCompletedPublishing(
+            final @NonNull InteractionLayerTracker iaTracker,
+            final @Nullable Command command,
+            final @NonNull Can<CommandSubscriber> enabledSubscribers,
+            final @NonNull Supplier<String> cannotPublishReasonSupplier) {
+
+        return enterCommandPublishing(iaTracker, command, enabledSubscribers, cannotPublishReasonSupplier, "completed");
+    }
+
+
+    private static SequenceHandle enterCommandPublishing(InteractionLayerTracker iaTracker, Command command, Can<CommandSubscriber> enabledSubscribers, Supplier<String> cannotPublishReasonSupplier, String verb) {
         if(!XrayUi.isXrayEnabled()) {
             return null;
         }
@@ -60,10 +84,11 @@ final class _Xray {
         val cannotPublishReason = cannotPublishReasonSupplier.get();
         val canPublish = cannotPublishReason==null;
         val enteringLabel = canPublish
-                ? String.format("publishing command to %d subscriber(s):\n%s",
+                ? String.format("publishing command %s to %d subscriber(s):\n%s",
+                        verb,
                         enabledSubscribers.size(),
                         toText(command))
-                : String.format("not publishing command:\n%s", cannotPublishReason);
+                : String.format("not publishing command %s:\n%s", verb, cannotPublishReason);
 
         val handleIfAny = XrayUtil.createSequenceHandle(iaTracker, "cmd-publisher");
         handleIfAny.ifPresent(handle->{
@@ -85,7 +110,6 @@ final class _Xray {
         });
 
         return handleIfAny.orElse(null);
-
     }
 
     // -- EXECUTION
diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java
index f7bcfafcec..79b517cd9e 100644
--- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java
@@ -19,7 +19,6 @@
 package org.apache.causeway.core.runtimeservices.session;
 
 import java.io.File;
-import java.sql.Timestamp;
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
@@ -374,6 +373,7 @@ implements
     }
 
     private void preInteractionClosed(final CausewayInteraction interaction) {
+
         completeAndPublishCurrentCommand();
 
         RuntimeException flushException = null;
@@ -441,7 +441,7 @@ implements
             // the guard is in case we're here as the result of a redirect following a previous exception;just ignore.
 
             val priorInteractionExecution = interaction.getPriorExecution();
-            final Timestamp completedAt =
+            val completedAt =
                     priorInteractionExecution != null
                     ?
                         // copy over from the most recent (which will be the top-level) interaction
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/BackgroundService.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/BackgroundService.java
index 7af674425a..5eba5af1d2 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/BackgroundService.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/BackgroundService.java
@@ -112,7 +112,6 @@ public class BackgroundService {
     public static class PersistCommandExecutorService implements ExecutorService {
 
         @Inject CommandLogEntryRepository<? extends CommandLogEntry> commandLogEntryRepository;
-        @Inject InteractionService interactionService;
 
         private final static JavaSqlJaxbAdapters.TimestampToXMLGregorianCalendarAdapter gregorianCalendarAdapter  = new JavaSqlJaxbAdapters.TimestampToXMLGregorianCalendarAdapter();;
 
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLog.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLog.java
index 50f8c56a78..13f48d5b5e 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLog.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLog.java
@@ -22,6 +22,9 @@ import javax.annotation.Priority;
 import javax.inject.Inject;
 import javax.inject.Named;
 
+import org.apache.causeway.applib.services.clock.ClockService;
+import org.apache.causeway.applib.services.repository.RepositoryService;
+import org.apache.causeway.schema.cmd.v2.CommandDto;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Service;
 
@@ -57,7 +60,9 @@ public class CommandSubscriberForCommandLog implements CommandSubscriber {
     static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandSubscriberForCommandLog";
 
     final CommandLogEntryRepository<? extends CommandLogEntry> commandLogEntryRepository;
+    final RepositoryService repositoryService;
     final CausewayConfiguration causewayConfiguration;
+    final ClockService clockService;
 
     @Override
     public boolean isEnabled() {
@@ -65,8 +70,7 @@ public class CommandSubscriberForCommandLog implements CommandSubscriber {
     }
 
     @Override
-    public void onCompleted(final Command command) {
-
+    public void onReady(Command command) {
         if (!isEnabled()) {
             return;
         }
@@ -74,6 +78,7 @@ public class CommandSubscriberForCommandLog implements CommandSubscriber {
         val existingCommandLogEntryIfAny =
                 commandLogEntryRepository.findByInteractionId(command.getInteractionId());
         if(existingCommandLogEntryIfAny.isPresent()) {
+
             val commandLogEntry = existingCommandLogEntryIfAny.get();
             switch (commandLogEntry.getExecuteIn()) {
                 case FOREGROUND:
@@ -95,10 +100,80 @@ public class CommandSubscriberForCommandLog implements CommandSubscriber {
                     // need to do anything else.
                     break;
             }
+
         } else {
-            val parentInteractionId = command.getParentInteractionId();
+            val parentInteractionId = command.getParentInteractionId(); // will be null in most (all?) cases
             commandLogEntryRepository.createEntryAndPersist(command, parentInteractionId, ExecuteIn.FOREGROUND);
         }
+
+    }
+
+    @Override
+    public void onStarted(Command command) {
+        if (!isEnabled()) {
+            return;
+        }
+
+        val existingCommandLogEntryIfAny =
+                commandLogEntryRepository.findByInteractionId(command.getInteractionId());
+        existingCommandLogEntryIfAny.ifPresent(commandLogEntry -> {
+            commandLogEntry.setStartedAt(clockService.getClock().nowAsJavaSqlTimestamp());
+        });
+
+    }
+
+    @Override
+    public void onCompleted(final Command command) {
+
+        if (!isEnabled()) {
+            return;
+        }
+
+        val existingCommandLogEntryIfAny =
+                commandLogEntryRepository.findByInteractionId(command.getInteractionId());
+
+        val onlyIfSystemChanged = causewayConfiguration.getExtensions().getCommandLog().getPublishPolicy().isOnlyIfSystemChanged();
+        if (onlyIfSystemChanged && !command.isSystemStateChanged()) {
+
+            // we don't need the CommandLogEntry after all.
+            existingCommandLogEntryIfAny.ifPresent(repositoryService::remove);
+
+        } else {
+
+            existingCommandLogEntryIfAny.ifPresent(commandLogEntry -> {
+                CommandDto commandDto = commandLogEntry.getCommandDto();
+                commandLogEntry
+                        .setResult(null);
+            });
+            if(existingCommandLogEntryIfAny.isPresent()) {
+                val commandLogEntry = existingCommandLogEntryIfAny.get();
+                switch (commandLogEntry.getExecuteIn()) {
+                    case FOREGROUND:
+                        // this isn't expected to happen ... we just log the fact if it does
+                        if(log.isWarnEnabled()) {
+                            val existingCommandDto = existingCommandLogEntryIfAny.get().getCommandDto();
+
+                            val existingCommandDtoXml = Try.call(()->CommandDtoUtils.dtoMapper().toString(existingCommandDto))
+                                    .getValue().orElse("Dto to Xml failure");
+                            val commandDtoXml = Try.call(()->CommandDtoUtils.dtoMapper().toString(command.getCommandDto()))
+                                    .getValue().orElse("Dto to Xml failure");
+
+                            log.warn("existing: \n{}", existingCommandDtoXml);
+                            log.warn("proposed: \n{}", commandDtoXml);
+                        }
+                        break;
+                    case BACKGROUND:
+                        // this is expected behaviour; the command was already persisted when initially scheduled; we don't
+                        // need to do anything else.
+                        break;
+                }
+            } else {
+                val parentInteractionId = command.getParentInteractionId();
+                commandLogEntryRepository.createEntryAndPersist(command, parentInteractionId, ExecuteIn.FOREGROUND);
+            }
+
+        }
+
     }
 
 }
diff --git a/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/publishing/subscriber/CommandSubscriberForTesting.java b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/publishing/subscriber/CommandSubscriberForTesting.java
index 6f78b4d89f..8940bc1d53 100644
--- a/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/publishing/subscriber/CommandSubscriberForTesting.java
+++ b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/publishing/subscriber/CommandSubscriberForTesting.java
@@ -53,17 +53,32 @@ implements CommandSubscriber {
         log.info("about to initialize");
     }
 
+    @Override
+    public void onReady(Command command) {
+        on("readyCommands", command);
+        log.debug("publish ready command {}", ()->command.getCommandDto());
+    }
+
+    @Override
+    public void onStarted(Command command) {
+        on("startedCommands", command);
+        log.debug("publish started command {}", ()->command.getCommandDto());
+    }
+
     @Override
     public void onCompleted(Command command) {
+        on("completedCommands", command);
+        log.debug("publish completed command {}", ()->command.getCommandDto());
+    }
+
 
+    private void on(String verb, Command command) {
         @SuppressWarnings("unchecked")
-        val publishedCommands =
-        (List<Command>) kvStore.get(this, "publishedCommands").orElseGet(ArrayList::new);
+        val commands = (List<Command>) kvStore.get(this, verb).orElseGet(ArrayList::new);
 
-        publishedCommands.add(command);
+        commands.add(command);
 
-        kvStore.put(this, "publishedCommands", publishedCommands);
-        log.debug("publish command {}", ()->command.getCommandDto());
+        kvStore.put(this, verb, commands);
     }
 
     // -- UTILITIES
@@ -71,7 +86,7 @@ implements CommandSubscriber {
     @SuppressWarnings("unchecked")
     public static Can<Command> getPublishedCommands(KVStoreForTesting kvStore) {
         return Can.ofCollection(
-                (List<Command>) kvStore.get(CommandSubscriberForTesting.class, "publishedCommands")
+                (List<Command>) kvStore.get(CommandSubscriberForTesting.class, "completedCommands")
                 .orElse(null));
     }