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

ahuber pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit d2944d6ae04905e4934ab996cb849602a4fa4503
Author: Andi Huber <[email protected]>
AuthorDate: Fri Jun 27 11:07:13 2025 +0200

    CAUSEWAY-3883: re-instante async execution finishing stage
---
 .../services/wrapper/control/SyncControl.java      | 18 ++++-
 .../wrapper/listeners/InteractionAdapter.java      |  9 ---
 .../wrapper/listeners/InteractionListener.java     | 16 -----
 ...mandRecord.java => AsyncExecutionFinisher.java} | 19 ++---
 ...syncExecutorService.java => AsyncExecutor.java} |  5 +-
 .../wrapper/AsyncProxyInternal.java                | 14 ++--
 .../wrapper/WrapperFactoryDefault.java             | 34 +++++----
 .../wrapper/handlers/CommandRecordFactory.java     | 52 --------------
 .../handlers/DomainObjectInvocationHandler.java    |  9 +--
 .../wrapper/handlers/ProxyGenerator.java           |  8 +--
 .../wrapper/internal/CommandRecord.java            | 84 ++++++++++++++++++++++
 .../ProxyCreatorTestUsingCodegenPlugin.java        |  3 +-
 .../commandlog/applib/dom/BackgroundService.java   |  4 +-
 13 files changed, 153 insertions(+), 122 deletions(-)

diff --git 
a/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/control/SyncControl.java
 
b/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/control/SyncControl.java
index 15257dabd44..9af288fad15 100644
--- 
a/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/control/SyncControl.java
+++ 
b/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/control/SyncControl.java
@@ -52,12 +52,26 @@ public record SyncControl(
          */
         ExceptionHandler exceptionHandler) {
 
+    //TODO can this be further simplified, or is there already an API we can 
reuse?
     @FunctionalInterface
     public interface CommandListener {
         public void onCommand(
+                /**
+                 * The unique {@link Command#getInteractionId() interactionId} 
of the parent {@link Command}, which is to say the
+                 * {@link Command} that was active in the original interaction 
where
+                 * {@link 
org.apache.causeway.applib.services.wrapper.WrapperFactory#asyncWrap(Object, 
AsyncControl)} (or its brethren)
+                 * was called.
+                 *
+                 * <p>This can be useful for custom systems to link parent and 
child commands together.
+                 */
+                UUID parentInteractionId,
                 InteractionContext interactionContext,
-                CommandDto commandDto,
-                UUID parentInteractionId);
+                /**
+                 * Details of the actual child command (action or property 
edit) to be performed.
+                 *
+                 * <p>Ultimately this can be handed onto the {@link 
org.apache.causeway.applib.services.command.CommandExecutorService}.
+                 */
+                CommandDto commandDto);
     }
 
     public static SyncControl defaults() {
diff --git 
a/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/listeners/InteractionAdapter.java
 
b/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/listeners/InteractionAdapter.java
index cc57cb96b4c..33ffe9ebcd2 100644
--- 
a/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/listeners/InteractionAdapter.java
+++ 
b/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/listeners/InteractionAdapter.java
@@ -69,15 +69,6 @@ public void collectionUsable(final CollectionUsabilityEvent 
ev) {
     public void collectionAccessed(final CollectionAccessEvent ev) {
     }
 
-//XXX[CAUSEWAY-3084] - removal of collection modification events
-//    @Override
-//    public void collectionAddedTo(final CollectionAddToEvent ev) {
-//    }
-//
-//    @Override
-//    public void collectionRemovedFrom(final CollectionRemoveFromEvent ev) {
-//    }
-
     @Override
     public void collectionMethodInvoked(final CollectionMethodEvent 
interactionEvent) {
     }
diff --git 
a/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/listeners/InteractionListener.java
 
b/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/listeners/InteractionListener.java
index 31e133dfeaa..6a686bd5ab7 100644
--- 
a/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/listeners/InteractionListener.java
+++ 
b/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/listeners/InteractionListener.java
@@ -126,22 +126,6 @@ public interface InteractionListener {
      */
     void collectionAccessed(CollectionAccessEvent ev);
 
-//XXX[CAUSEWAY-3084] - removal of collection modification events
-//    /**
-//     * An object was added to the collection (or an attempt to add it was 
made).
-//     *
-//     * @param ev
-//     */
-//    void collectionAddedTo(CollectionAddToEvent ev);
-//
-//    /**
-//     * An object was removed from the collection (or an attempt to remove it 
was
-//     * made).
-//     *
-//     * @param ev
-//     */
-//    void collectionRemovedFrom(CollectionRemoveFromEvent ev);
-
     /**
      * A method of a collection (such as <tt>isEmpty()</tt> or 
<tt>size()</tt>) has been invoked.
      *
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/CommandRecord.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncExecutionFinisher.java
similarity index 64%
rename from 
core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/CommandRecord.java
rename to 
core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncExecutionFinisher.java
index ee5235fef70..804c0fdb7d3 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/CommandRecord.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncExecutionFinisher.java
@@ -16,16 +16,19 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.causeway.core.runtimeservices.wrapper.handlers;
+package org.apache.causeway.core.runtimeservices.wrapper;
 
-import java.util.UUID;
+import org.apache.causeway.applib.services.repository.RepositoryService;
+import org.apache.causeway.applib.services.wrapper.WrapperFactory;
 
-import org.apache.causeway.applib.services.iactnlayer.InteractionContext;
-import org.apache.causeway.schema.cmd.v2.CommandDto;
+record AsyncExecutionFinisher(
+        WrapperFactory wrapperFactory,
+        RepositoryService repositoryService
+        ) {
 
-record CommandRecord(
-        InteractionContext interactionContext,
-        CommandDto commandDto,
-        UUID parentInteractionId) {
+    public <T> T finish(T t) {
+        var pojo = wrapperFactory.unwrap(t);
+        return repositoryService.detach(pojo);
+    }
 
 }
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncExecutorService.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncExecutor.java
similarity index 97%
rename from 
core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncExecutorService.java
rename to 
core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncExecutor.java
index 886e61edafe..f85921c92b7 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncExecutorService.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncExecutor.java
@@ -36,7 +36,10 @@
 import org.apache.causeway.commons.functional.ThrowingRunnable;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
 
-record AsyncExecutorService(
+/**
+ * Implements {@link ExecutorService} providing an interaction and optional 
transaction scope for each invocation.
+ */
+record AsyncExecutor(
         InteractionService interactionService,
         TransactionService transactionService,
         InteractionContext interactionContext,
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncProxyInternal.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncProxyInternal.java
index 682f4bc0d2a..502486f42a0 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncProxyInternal.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncProxyInternal.java
@@ -24,10 +24,13 @@
 import java.util.function.Function;
 
 import org.apache.causeway.applib.services.wrapper.WrapperFactory.AsyncProxy;
-import org.apache.causeway.core.runtime.wrap.WrappingObject;
 
 //TODO this is just a proof of concept; chaining makes non sense once future 
is no longer a proxy
-record AsyncProxyInternal<T>(CompletableFuture<T> future, AsyncExecutorService 
executor) implements AsyncProxy<T> {
+record AsyncProxyInternal<T>(
+        CompletableFuture<T> future,
+        AsyncExecutor executor,
+        AsyncExecutionFinisher finisher) implements AsyncProxy<T> {
+
     @Override public AsyncProxy<Void> thenAcceptAsync(Consumer<? super T> 
action) {
         return map(in->in.thenAcceptAsync(action, executor));
     }
@@ -40,17 +43,14 @@ record AsyncProxyInternal<T>(CompletableFuture<T> future, 
AsyncExecutorService e
         return map(in->in.orTimeout(timeout, unit));
     }
 
-    @SuppressWarnings("unchecked")
     @Override public T join() {
         var t = future.join();
-        return t instanceof WrappingObject wrappingObject
-            ? (T) WrappingObject.getOrigin(wrappingObject).pojo()
-            : t;
+        return finisher.finish(t);
     }
 
     // -- HELPER
 
     private <U> AsyncProxy<U> map(Function<CompletableFuture<T>, 
CompletableFuture<U>> fn) {
-        return new AsyncProxyInternal<>(fn.apply(future), executor);
+        return new AsyncProxyInternal<>(fn.apply(future), executor, finisher);
     }
 }
\ No newline at end of file
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java
index 359e28aeba5..cbfa29c7b83 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java
@@ -81,8 +81,10 @@
 import 
org.apache.causeway.core.runtimeservices.wrapper.dispatchers.InteractionEventDispatcher;
 import 
org.apache.causeway.core.runtimeservices.wrapper.dispatchers.InteractionEventDispatcherTypeSafe;
 import 
org.apache.causeway.core.runtimeservices.wrapper.handlers.ProxyGenerator;
+import org.apache.causeway.core.runtimeservices.wrapper.internal.CommandRecord;
 
 import lombok.Getter;
+import lombok.experimental.Accessors;
 
 /**
  * Default implementation of {@link WrapperFactory}.
@@ -112,12 +114,15 @@ public class WrapperFactoryDefault
     private ExecutorService commonExecutorService;
     private ProxyGenerator proxyGenerator;
 
+    @Getter(lazy = true) @Accessors(fluent=true)
+    private final AsyncExecutionFinisher executionFinisher = new 
AsyncExecutionFinisher(this, getRepositoryService());
+
     @PostConstruct
     public void init() {
 
         this.commonExecutorService = newCommonExecutorService();
 
-        this.proxyGenerator = new ProxyGenerator(proxyFactoryService, 
interactionIdGenerator);
+        this.proxyGenerator = new ProxyGenerator(proxyFactoryService, new 
CommandRecord.Factory(interactionIdGenerator));
 
         putDispatcher(ObjectTitleEvent.class, 
InteractionListener::objectTitleRead);
         putDispatcher(PropertyVisibilityEvent.class, 
InteractionListener::propertyVisible);
@@ -127,9 +132,6 @@ public void init() {
         putDispatcher(CollectionVisibilityEvent.class, 
InteractionListener::collectionVisible);
         putDispatcher(CollectionUsabilityEvent.class, 
InteractionListener::collectionUsable);
         putDispatcher(CollectionAccessEvent.class, 
InteractionListener::collectionAccessed);
-//XXX[CAUSEWAY-3084] - removal of collection modification events
-//        putDispatcher(CollectionAddToEvent.class, 
InteractionListener::collectionAddedTo);
-//        putDispatcher(CollectionRemoveFromEvent.class, 
InteractionListener::collectionRemovedFrom);
         putDispatcher(ActionVisibilityEvent.class, 
InteractionListener::actionVisible);
         putDispatcher(ActionUsabilityEvent.class, 
InteractionListener::actionUsable);
         putDispatcher(ActionArgumentEvent.class, 
InteractionListener::actionArgument);
@@ -225,18 +227,16 @@ public boolean isWrapper(final Object obj) {
     }
 
     @Override
-    public <T> T unwrap(final T possibleWrappedDomainObject) {
-        if(isWrapper(possibleWrappedDomainObject)) {
-            var wrappingObject = (WrappingObject) possibleWrappedDomainObject;
-            return 
_Casts.uncheckedCast(wrappingObject.__causeway_origin().pojo());
-        }
-        return possibleWrappedDomainObject;
+    public <T> T unwrap(final T t) {
+        return t instanceof WrappingObject wrappingObject
+                ? 
_Casts.uncheckedCast(wrappingObject.__causeway_origin().pojo())
+                : t;
     }
 
     // -- ASYNC WRAPPING
 
-    AsyncExecutorService asyncExecutorService(AsyncControl asyncControl) {
-        return new AsyncExecutorService(
+    AsyncExecutor asyncExecutor(AsyncControl asyncControl) {
+        return new AsyncExecutor(
                 interactionServiceProvider.get(),
                 transactionServiceProvider.get(),
                 asyncControl.override(InteractionContext.builder().build()),
@@ -245,12 +245,17 @@ AsyncExecutorService asyncExecutorService(AsyncControl 
asyncControl) {
                     .orElse(commonExecutorService));
     }
 
+    AsyncExecutionFinisher finisher() {
+        return null;
+    }
+
     @Override
     public <T> AsyncProxy<T> asyncWrap(T domainObject, AsyncControl 
asyncControl) {
         var proxy = wrap(domainObject, asyncControl.syncControl());
         return new AsyncProxyInternal<>(
                 CompletableFuture.completedFuture(proxy),
-                asyncExecutorService(asyncControl));
+                asyncExecutor(asyncControl),
+                executionFinisher());
     }
 
     @Override
@@ -261,7 +266,8 @@ public <T> AsyncProxy<T> asyncWrapMixin(
         var proxy = wrapMixin(mixinClass, mixee, asyncControl.syncControl());
         return new AsyncProxyInternal<>(
                 CompletableFuture.completedFuture(proxy),
-                asyncExecutorService(asyncControl));
+                asyncExecutor(asyncControl),
+                executionFinisher());
     }
 
     // -- LISTENERS
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/CommandRecordFactory.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/CommandRecordFactory.java
deleted file mode 100644
index 6b19783527a..00000000000
--- 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/CommandRecordFactory.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.apache.causeway.core.runtimeservices.wrapper.handlers;
-
-import org.apache.causeway.commons.collections.Can;
-import org.apache.causeway.core.metamodel.interactions.InteractionHead;
-import org.apache.causeway.core.metamodel.object.ManagedObject;
-import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
-import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
-import org.apache.causeway.core.runtimeservices.session.InteractionIdGenerator;
-
-record CommandRecordFactory(
-        InteractionIdGenerator interactionIdGenerator) {
-
-    public CommandRecord forAction(InteractionHead head, ObjectAction act, 
Can<ManagedObject> args) {
-        return new CommandRecord(
-                
act.getInteractionService().currentInteractionContextElseFail(),
-                act.getCommandDtoFactory()
-                        .asCommandDto(interactionIdGenerator.interactionId(), 
head, act, args),
-                act.getInteractionService().currentInteractionElseFail()
-                        .getCommand()
-                        .getInteractionId());
-    }
-
-    public CommandRecord forProperty(InteractionHead head, OneToOneAssociation 
prop, ManagedObject arg) {
-        return new CommandRecord(
-                
prop.getInteractionService().currentInteractionContextElseFail(),
-                prop.getCommandDtoFactory()
-                        .asCommandDto(interactionIdGenerator.interactionId(), 
head, prop, arg),
-                prop.getInteractionService().currentInteractionElseFail()
-                        .getCommand()
-                        .getInteractionId());
-    }
-
-
-}
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java
index 2b1f34deea9..f1282cec18a 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java
@@ -65,6 +65,7 @@
 import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
 import org.apache.causeway.core.runtime.wrap.WrapperInvocationHandler;
 import org.apache.causeway.core.runtime.wrap.WrappingObject;
+import org.apache.causeway.core.runtimeservices.wrapper.internal.CommandRecord;
 
 import lombok.Getter;
 import lombok.SneakyThrows;
@@ -82,12 +83,12 @@ final class DomainObjectInvocationHandler
 
     private final ObjectSpecification targetSpec;
     private final ProxyGenerator proxyGenerator;
-    private final CommandRecordFactory commandRecordFactory;
+    private final CommandRecord.Factory commandRecordFactory;
 
     DomainObjectInvocationHandler(
             final ObjectSpecification targetSpec,
             final ProxyGenerator proxyGenerator,
-            final CommandRecordFactory commandRecordFactory) {
+            final CommandRecord.Factory commandRecordFactory) {
         this.targetSpec = targetSpec;
         this.classMetaData = 
WrapperInvocationHandler.ClassMetaData.of(targetSpec.getCorrespondingClass());
         this.proxyGenerator = proxyGenerator;
@@ -534,9 +535,9 @@ private void handleCommandListeners(
             wrapperInvocation.syncControl().commandListeners()
                 .forEach(listener->listener
                         .onCommand(
+                                commandRecord.parentInteractionId(),
                                 commandRecord.interactionContext(),
-                                commandRecord.commandDto(),
-                                commandRecord.parentInteractionId()));
+                                commandRecord.commandDto()));
         }
     }
 
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyGenerator.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyGenerator.java
index b5e2b424adf..be1cb99df6b 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyGenerator.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyGenerator.java
@@ -34,15 +34,11 @@
 import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
 import org.apache.causeway.core.runtime.wrap.WrapperInvocationHandler;
 import org.apache.causeway.core.runtime.wrap.WrappingObject;
-import org.apache.causeway.core.runtimeservices.session.InteractionIdGenerator;
+import org.apache.causeway.core.runtimeservices.wrapper.internal.CommandRecord;
 
 public record ProxyGenerator(
         @NonNull ProxyFactoryService proxyFactoryService,
-        @NonNull CommandRecordFactory commandRecordFactory) {
-
-    public ProxyGenerator(ProxyFactoryService proxyFactoryService, 
InteractionIdGenerator interactionIdGenerator) {
-        this(proxyFactoryService, new 
CommandRecordFactory(interactionIdGenerator));
-    }
+        CommandRecord.@NonNull Factory commandRecordFactory) {
 
     @SuppressWarnings("unchecked")
     public <T> T objectProxy(
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/internal/CommandRecord.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/internal/CommandRecord.java
new file mode 100644
index 00000000000..932dcdadb27
--- /dev/null
+++ 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/internal/CommandRecord.java
@@ -0,0 +1,84 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.core.runtimeservices.wrapper.internal;
+
+import java.util.UUID;
+
+import org.apache.causeway.applib.services.iactnlayer.InteractionContext;
+import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.core.metamodel.context.HasMetaModelContext;
+import org.apache.causeway.core.metamodel.interactions.InteractionHead;
+import org.apache.causeway.core.metamodel.object.ManagedObject;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
+import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
+import org.apache.causeway.core.runtimeservices.session.InteractionIdGenerator;
+import org.apache.causeway.schema.cmd.v2.CommandDto;
+
+public record CommandRecord(
+        /**
+         * The unique {@link Command#getInteractionId() interactionId} of the 
parent {@link Command}, that is the
+         * {@link Command} that was active in the original interaction where
+         * {@link 
org.apache.causeway.applib.services.wrapper.WrapperFactory#asyncWrap(Object, 
AsyncControl)} (or its brethren)
+         * was called.
+         *
+         * <p>This can be useful for custom implementations of {@link 
ExecutorService} that use the commandlog
+         * extension's <code>CommandLogEntry</code>, to link parent and child 
commands together.
+         */
+        UUID parentInteractionId,
+        InteractionContext interactionContext,
+        /**
+         * Details of the actual child command (action or property edit) to be 
performed.
+         *
+         * <p>(Ultimately this is handed onto the {@link 
org.apache.causeway.applib.services.command.CommandExecutorService}).
+         */
+         CommandDto commandDto) {
+
+    // -- FACTORY
+
+    public record Factory(
+            InteractionIdGenerator interactionIdGenerator) {
+
+        public CommandRecord forAction(InteractionHead head, ObjectAction act, 
Can<ManagedObject> args) {
+            return new CommandRecord(
+                currentParentInteractionId(act),
+                currentInteractionContext(act),
+                
act.getCommandDtoFactory().asCommandDto(interactionIdGenerator.interactionId(), 
head, act, args));
+        }
+
+        public CommandRecord forProperty(InteractionHead head, 
OneToOneAssociation prop, ManagedObject arg) {
+            return new CommandRecord(
+                currentParentInteractionId(prop),
+                currentInteractionContext(prop),
+                
prop.getCommandDtoFactory().asCommandDto(interactionIdGenerator.interactionId(),
 head, prop, arg));
+        }
+
+        // -- HELPER
+
+        private static InteractionContext 
currentInteractionContext(HasMetaModelContext mmc) {
+            return 
mmc.getInteractionService().currentInteractionContextElseFail();
+        }
+        private static UUID currentParentInteractionId(HasMetaModelContext 
mmc) {
+            return mmc.getInteractionService().currentInteractionElseFail()
+                    .getCommand()
+                    .getInteractionId();
+        }
+
+    }
+
+}
diff --git 
a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyCreatorTestUsingCodegenPlugin.java
 
b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyCreatorTestUsingCodegenPlugin.java
index d5f7cd91abb..1a575790f8d 100644
--- 
a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyCreatorTestUsingCodegenPlugin.java
+++ 
b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyCreatorTestUsingCodegenPlugin.java
@@ -31,10 +31,11 @@
 import 
org.apache.causeway.core.codegen.bytebuddy.services.ProxyFactoryServiceByteBuddy;
 import org.apache.causeway.core.runtime.wrap.WrappingObject;
 import org.apache.causeway.core.runtimeservices.RuntimeServicesTestAbstract;
+import org.apache.causeway.core.runtimeservices.wrapper.internal.CommandRecord;
 
 class ProxyCreatorTestUsingCodegenPlugin extends RuntimeServicesTestAbstract {
 
-    private ProxyGenerator proxyGenerator = new ProxyGenerator(new 
ProxyFactoryServiceByteBuddy(), new CommandRecordFactory(null));
+    private ProxyGenerator proxyGenerator = new ProxyGenerator(new 
ProxyFactoryServiceByteBuddy(), new CommandRecord.Factory(null));
 
     @DomainObject(nature = Nature.VIEW_MODEL)
     public static class Employee {
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 04571a47f23..103ec061361 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
@@ -107,9 +107,9 @@ record CommandPersistor(CommandLogEntryRepository 
commandLogEntryRepository) imp
 
         @Override
         public void onCommand(
+                final UUID parentInteractionId,
                 final InteractionContext interactionContext,
-                final CommandDto commandDto,
-                final UUID parentInteractionId) {
+                final CommandDto commandDto) {
 
             // we'll mutate the commandDto in line with the callable, then
             // create the CommandLogEntry from that commandDto

Reply via email to