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 2026/09/24 16:22:23 UTC
(causeway) 29/41: CAUSEWAY-4066: proposes application priming hooks
This is an automated email from the ASF dual-hosted git repository.
danhaywood pushed a commit to branch CAUSEWAY-4066
in repository https://gitbox.apache.org/repos/asf/causeway.git
commit 421d977589f66d3374f402a326e8469642da40cb
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Sep 24 12:44:43 2026 +0100
CAUSEWAY-4066: proposes application priming hooks
---
.../add-application-priming-hooks/.openspec.yaml | 2 +
.../add-application-priming-hooks/design.md | 165 +++++++++++++++++++++
.../add-application-priming-hooks/proposal.md | 32 ++++
.../specs/application-priming-hooks/spec.md | 129 ++++++++++++++++
.../changes/add-application-priming-hooks/tasks.md | 33 +++++
5 files changed, 361 insertions(+)
diff --git a/openspec/changes/add-application-priming-hooks/.openspec.yaml b/openspec/changes/add-application-priming-hooks/.openspec.yaml
new file mode 100644
index 00000000000..75289e4b73f
--- /dev/null
+++ b/openspec/changes/add-application-priming-hooks/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-09-24
diff --git a/openspec/changes/add-application-priming-hooks/design.md b/openspec/changes/add-application-priming-hooks/design.md
new file mode 100644
index 00000000000..79d4732c48e
--- /dev/null
+++ b/openspec/changes/add-application-priming-hooks/design.md
@@ -0,0 +1,165 @@
+## Context
+
+Causeway applications can avoid repeated lazy-loading queries by issuing application-specific bulk reads before an action traverses an object graph or before Wicket renders a large entity page.
+The framework currently has no lifecycle hook that lets an application associate such reads with selected actions or entity types.
+Applications could subscribe to action domain events or modify viewer components, but action events are configuration-dependent and viewer customization couples the optimization to internal Wicket code.
+
+The hook is an opportunistic performance facility rather than a diagnostic or fetching abstraction.
+The application remains responsible for choosing persistence queries that populate its ORM persistence context, and Causeway neither describes nor verifies the resulting object graph.
+
+Registration requires the completed metamodel because a Java class must resolve to an exact Causeway logical type and a local action name must resolve to a domain-facing action identity.
+Callbacks execute concurrently across requests as ordinary singleton application services, while their repository dependencies use the active request or interaction persistence context.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Let applications register synchronous cache-priming callbacks for selected domain actions and Wicket entity-page root types.
+- Route callbacks through precomputed exact-match registries without requiring callbacks to inspect or parse logical identifiers.
+- Resolve registration keys from domain classes and local action logical names through the initialized metamodel.
+- Fail application startup for invalid registrations rather than silently leaving an intended optimization inactive.
+- Invoke callbacks early enough that subsequent action or page access can reuse objects loaded into the current ORM persistence context.
+- Keep the callback API independent of JDO, JPA, Wicket internals, and any particular repository implementation.
+
+**Non-Goals:**
+
+- Detect, diagnose, count, trace, or automatically repair N+1 queries.
+- Define which object graph an application should load or provide an ORM-independent fetch-plan language.
+- Prime collection rows individually or introduce a collection-view callback.
+- Match registrations by wildcard, annotation, assignability, superclass, interface, or subtype traversal.
+- Replace action domain events, lifecycle callbacks, or viewer extension APIs.
+- Support asynchronous callbacks or callbacks that outlive the current invocation or rendering request.
+
+## Decisions
+
+### Introduce registrar, registry, and callback contracts in applib
+
+Add a `PrimingRegistrar` SPI whose implementations receive a `PrimingRegistry` after the metamodel has loaded.
+The registry exposes singular and plural action-registration operations and an entity-view registration operation conceptually equivalent to:
+
+```java
+<T> void action(
+ Class<T> domainType,
+ String actionLogicalName,
+ ActionPrimer<? super T> primer);
+
+<T> void actions(
+ Class<T> domainType,
+ Collection<String> actionLogicalNames,
+ ActionPrimer<? super T> primer);
+
+<T> void view(
+ Class<T> domainType,
+ ViewPrimer<? super T> primer);
+```
+
+`ActionPrimer<T>` receives a typed domain-facing target and immutable `ActionArguments`.
+`ViewPrimer<T>` receives the typed root domain object.
+The registration's `Class<T>` enables the registry to perform the target cast, so application callbacks do not need to cast from `Object`.
+
+The alternative of defining one service implementation per identifier was rejected because every invocation would need to scan services or ask each service whether it applies.
+The alternative of using annotations on primer methods was rejected because reflective argument binding and lifecycle discovery would add complexity without improving runtime routing.
+
+### Derive exact keys from the initialized metamodel
+
+For action registration, resolve the supplied class to its exact object specification, resolve each supplied local action logical name on that specification, and store the resulting domain-facing logical member identifier as the lookup key.
+For view registration, resolve the supplied class to its exact object specification and store that specification's logical type name as the lookup key.
+Registration therefore looks like:
+
+```java
+registry.actions(
+ Invoice.class,
+ Set.of("approve", "recalculate"),
+ this::primeInvoice);
+
+registry.view(
+ Invoice.class,
+ this::primeInvoiceView);
+```
+
+The application does not supply `estatio.Invoice#approve` or `estatio.Invoice`, and the callback receives neither value.
+A contributed action is registered against its domain target or mixee class and its local action name, not against the mixin implementation class.
+
+The registry rejects a class that has no exact metamodel specification and logical type, and it rejects an unknown or malformed local action name.
+It does not search up or down the Java class hierarchy and does not expand a registration to related logical types.
+This exact behavior avoids surprising activation and leaves hierarchy matching for a future independently specified enhancement.
+
+The alternative of accepting full logical identifiers was rejected because it duplicates the logical type already represented by the class and makes registrations vulnerable to string construction errors.
+
+### Populate once after metamodel loading and then freeze
+
+The registry implementation listens for completion of metamodel loading, obtains all `PrimingRegistrar` services, and lets them contribute registrations.
+It validates each contribution while the metamodel is available and then freezes two immutable multimaps:
+
+```text
+logical member identifier -> action primer registrations
+logical type name -> view primer registrations
+```
+
+A registration attempt after freezing fails rather than mutating behavior while requests are active.
+Invalid registration aborts startup with a message identifying the registrar, class, and action name where applicable.
+
+Multiple callbacks may be registered for one key and all of them execute.
+Callbacks must not depend on their relative order, although registrar discovery and callback storage remain deterministic for repeatable tests.
+
+The alternative of lazy registration on first invocation was rejected because invalid registrations would escape startup validation and could first fail under production load.
+
+### Route action priming immediately before domain method execution
+
+Integrate dispatch with ordinary `ActionExecutor` execution after action validation and any `EXECUTING` domain-event argument adjustment, but before invoking the domain method.
+The lookup uses the domain-facing logical member identifier, including the mixee identity for a contributed action.
+The callback target is the interaction owner or mixee rather than the transient mixin implementation instance.
+
+The primer executes synchronously in the same interaction, transaction, thread, and ORM persistence context as the action.
+Normal nested or wrapper-mediated action executions participate when they pass through ordinary action execution, while pass-through invocation and synthetic mixed-in property or collection access do not introduce separate priming calls.
+
+The alternative of passing `ActionInvocation` directly was rejected because its mixin target can be the transient mixin and its argument list need not represent post-event effective arguments.
+The alternative of invoking before validation was rejected because unavailable or invalid actions must not perform speculative application reads.
+
+### Represent arguments as an immutable positional value
+
+`ActionArguments` preserves action arity and exposes `size()`, positional `get(int)`, typed `get(int, Class<T>)`, and an unmodifiable list view.
+A parameter whose value is itself an array remains one positional argument rather than being flattened as it could be by a varargs callback signature.
+Typed access validates the requested type and reports an argument error with the index and expected type.
+
+Arguments remain positional because registration already identifies the exact action and the application therefore knows its parameter contract.
+Named or reflectively bound primer parameters were rejected as unnecessary additional API and metamodel coupling.
+
+### Prime Wicket entity pages by exact root logical type
+
+At the beginning of each relevant Wicket entity-page rendering request, resolve the page's root managed object and use its exact logical type name to select view callbacks.
+Invoke matching callbacks after object visibility authorization but before expensive title, layout, property, or collection preparation that can traverse the object graph.
+Use a request-local guard so each callback runs at most once for the same entity page during one rendering request, while allowing it to run again on later full or Ajax requests that have a new ORM persistence context.
+
+The callback receives the root domain object and can issue bulk repository queries for all collections and related objects likely to be used by the heavy page.
+No row-level callback is needed because the intended optimization is to prime the graph reachable from the page root.
+
+The alternative of invoking only during initial Wicket page construction was rejected because a stateful page can be rendered in a later request with a different persistence context.
+The alternative of invoking from generic model resolution was rejected because it would activate outside entity-page rendering and could recurse during callback queries.
+
+### Treat primers as application-owned synchronous reads
+
+Primer implementations must be synchronous, read-only, idempotent, and safe for concurrent invocation as singleton services.
+A callback exception propagates through the enclosing action or Wicket request because persistence failures can invalidate the active transaction and must not be hidden.
+When no callback is registered, dispatch performs only an exact map lookup and changes no application behavior.
+The framework does not add primer-specific tracing, metrics, caching, retries, or exception suppression.
+
+## Risks / Trade-offs
+
+- [Risk] A primer can over-fetch and make a request slower. → Keep registration opt-in and leave graph selection entirely under application control.
+- [Risk] A primer can perform writes or other side effects despite the read-only contract. → Document the contract prominently and propagate failures, while avoiding an unenforceable ORM-specific runtime check.
+- [Risk] A Wicket lifecycle path can prepare components before the callback runs. → Add focused lifecycle tests proving that initial and subsequent request callbacks precede representative expensive member access.
+- [Risk] Registering against a superclass may appear to imply subtype matching. → Validate and document exact logical-type matching and perform no hierarchy traversal.
+- [Risk] Multiple callbacks for one key can duplicate reads or develop ordering dependencies. → Invoke all callbacks deterministically, document that they must be independently idempotent, and test multi-registration behavior.
+- [Trade-off] Application code still handles positional action arguments. → Provide bounded typed access without introducing reflective binding or action-specific generated types.
+- [Trade-off] The view hook is currently Wicket-specific. → Keep the callback contract free of Wicket types so another viewer can adopt it later under a separate lifecycle specification.
+
+## Migration Plan
+
+The change is additive and requires no application migration.
+Applications opt in by adding a `PrimingRegistrar` service and registering selected callbacks.
+Rollback consists of removing the registrar or deploying the previous framework version, with no persisted data or configuration migration.
+
+## Open Questions
+
+None.
diff --git a/openspec/changes/add-application-priming-hooks/proposal.md b/openspec/changes/add-application-priming-hooks/proposal.md
new file mode 100644
index 00000000000..64b00d9622b
--- /dev/null
+++ b/openspec/changes/add-application-priming-hooks/proposal.md
@@ -0,0 +1,32 @@
+## Why
+
+Applications can suffer N+1 persistence access when an action or a large Wicket entity page traverses an object graph that has not been loaded efficiently into the ORM persistence context.
+Causeway needs an opt-in application hook that can bulk-load the likely-needed graph immediately before that work, without coupling framework code to application-specific persistence queries.
+
+## What Changes
+
+- Add an application registrar SPI for associating action and view priming callbacks with metamodel identities during startup.
+- Register action primers using a domain class and one or more local action logical names, without requiring callbacks to parse logical member identifiers.
+- Register entity-view primers using only a domain class, whose logical type is resolved from the initialized metamodel.
+- Build and freeze exact-match action and view registries after validating every registration.
+- Invoke matching action primers within the current persistence context immediately before ordinary domain action execution.
+- Invoke matching view primers before preparing and rendering a Wicket entity page during each relevant rendering request.
+- Pass action primers the domain-facing target and immutable positional action arguments, and pass view primers the page's root domain object.
+- Fail startup when a registered class has no exact logical type or when a registered local action name cannot be resolved.
+- Propagate callback failures while leaving unregistered actions and views unchanged.
+
+## Capabilities
+
+### New Capabilities
+
+- `application-priming-hooks`: Defines registration, metamodel validation, action and Wicket entity-view callback invocation, argument access, and failure behavior for application-defined ORM cache priming.
+
+### Modified Capabilities
+
+None.
+
+## Impact
+
+The change adds public applib registration and callback contracts, a runtime registry and action-execution integration, and Wicket entity-page integration.
+It requires focused metamodel, runtime, and Wicket tests plus application-facing documentation.
+It adds no ORM-specific API or external dependency, performs no automatic fetching, and introduces no collection-row priming, tracing, diagnostics, wildcard registration, or class-hierarchy matching.
diff --git a/openspec/changes/add-application-priming-hooks/specs/application-priming-hooks/spec.md b/openspec/changes/add-application-priming-hooks/specs/application-priming-hooks/spec.md
new file mode 100644
index 00000000000..2ec4f65c91b
--- /dev/null
+++ b/openspec/changes/add-application-priming-hooks/specs/application-priming-hooks/spec.md
@@ -0,0 +1,129 @@
+## ADDED Requirements
+
+### Requirement: Applications can register action primers by class and local action name
+The framework SHALL provide an application SPI for registering an action primer using a domain class and one or more local action logical names.
+The framework SHALL derive each complete domain-facing logical member identifier from the supplied class and local action name, and the primer SHALL NOT be required to inspect or parse that identifier.
+
+#### Scenario: Register one action primer
+- **WHEN** an application registers a primer for domain class `Invoice` and local action name `approve`
+- **THEN** the framework associates the primer with the metamodel action named `approve` on the exact logical type represented by `Invoice`
+
+#### Scenario: Register one primer for several actions
+- **WHEN** an application registers one primer for domain class `Invoice` and local action names `approve` and `recalculate`
+- **THEN** the framework associates that primer independently with both resolved domain-facing action identities
+
+#### Scenario: Register a contributed action
+- **WHEN** an application registers a primer using a domain target class and the local name of an action contributed to that class by a mixin
+- **THEN** the framework associates the primer with the action's domain-facing mixee identity rather than the mixin implementation type
+
+### Requirement: Applications can register entity-view primers by class
+The framework SHALL provide an application SPI for registering a view primer using only the domain class of a Wicket entity page's root object.
+The framework SHALL derive the registration key from the exact logical type assigned to that class by the metamodel.
+
+#### Scenario: Register a view primer
+- **WHEN** an application registers a view primer for domain class `Invoice`
+- **THEN** the framework associates the primer with the exact logical type represented by `Invoice` without requiring a logical type name from the application
+
+### Requirement: Priming registrations are validated and frozen at startup
+The framework SHALL collect priming registrations after metamodel loading, validate them against the completed metamodel, and freeze the resulting action and view lookup registries before serving application requests.
+Invalid registration SHALL fail application startup with a diagnostic that identifies the offending registration.
+
+#### Scenario: Registered class has no exact logical type
+- **WHEN** a registrar supplies a class that has no exact metamodel specification and logical type
+- **THEN** application startup fails instead of searching its superclasses, interfaces, or subclasses
+
+#### Scenario: Local action name cannot be resolved
+- **WHEN** a registrar supplies a local action name that does not resolve on the exact metamodel specification for the supplied class
+- **THEN** application startup fails and identifies the class and local action name
+
+#### Scenario: Registration is attempted after startup registration closes
+- **WHEN** application code attempts to add a registration after the registry has been frozen
+- **THEN** the framework rejects the mutation and leaves the active lookup registry unchanged
+
+#### Scenario: Related logical type is rendered
+- **WHEN** a view primer is registered for a superclass or other domain class with its own logical type and an object of a different subtype logical type is rendered
+- **THEN** the framework does not select the primer by Java assignability or hierarchy traversal
+
+### Requirement: Matching action primers run before action methods
+For each ordinary domain action execution, the framework SHALL perform an exact lookup using the action's domain-facing logical member identifier and SHALL synchronously invoke every matching action primer before invoking the domain method.
+The primer SHALL run within the action's current interaction, transaction, thread, and ORM persistence context.
+
+#### Scenario: Matching ordinary action is invoked
+- **WHEN** an ordinary domain action has a matching registered primer and passes action validation
+- **THEN** the framework invokes the primer after any executing-phase argument adjustment and before invoking the domain action method
+
+#### Scenario: Contributed action is invoked
+- **WHEN** a contributed action has a matching registered primer
+- **THEN** the primer receives the mixed-in domain object as its target rather than the transient mixin implementation instance
+
+#### Scenario: Nested ordinary action is invoked
+- **WHEN** wrapper-mediated or nested execution invokes an ordinary domain action with a matching registration
+- **THEN** the framework invokes that action's matching primer as part of the nested execution
+
+#### Scenario: Pass-through or synthetic association access occurs
+- **WHEN** the framework performs pass-through invocation or synthetic mixed-in property or collection access
+- **THEN** it does not introduce a separate action-priming callback
+
+#### Scenario: Action has no registered primer
+- **WHEN** an ordinary domain action has no exact action-primer registration
+- **THEN** the framework invokes the action normally without executing an application priming callback
+
+### Requirement: Action primers receive immutable positional arguments
+An action primer SHALL receive the effective action arguments as an immutable positional value that preserves the action's arity.
+The argument API SHALL provide the argument count, untyped indexed access, type-checked indexed access, and an unmodifiable list view.
+
+#### Scenario: Primer reads a typed argument
+- **WHEN** a primer requests an argument at a valid index using a compatible required type
+- **THEN** the argument API returns that argument without requiring the primer to parse action metadata
+
+#### Scenario: Action parameter is an array
+- **WHEN** an action has one parameter whose value is an array
+- **THEN** the argument API represents that array as one positional argument rather than flattening its elements
+
+#### Scenario: Primer requests an incompatible argument type
+- **WHEN** a primer requests a positional argument using an incompatible required type
+- **THEN** the argument API fails with an error identifying the index and expected type
+
+#### Scenario: Primer attempts to modify the list view
+- **WHEN** a primer attempts to mutate the action arguments through their list view
+- **THEN** the argument API rejects the mutation
+
+### Requirement: Matching view primers run before Wicket entity-page preparation
+For each relevant Wicket entity-page rendering request, the framework SHALL perform an exact lookup using the root object's logical type and SHALL synchronously invoke every matching view primer after object visibility authorization but before page preparation traverses the root object's title, layout, properties, or collections.
+The primer SHALL run within the rendering request's current thread and ORM persistence context.
+
+#### Scenario: Initial entity-page rendering
+- **WHEN** Wicket initially renders an authorized entity page whose exact root logical type has a registered view primer
+- **THEN** the framework invokes the primer before preparing the expensive page content
+
+#### Scenario: Later rendering request
+- **WHEN** the same stateful entity page participates in a later full or Ajax rendering request with a matching registration
+- **THEN** the framework invokes the primer for the current request so it can populate that request's ORM persistence context
+
+#### Scenario: Wicket lifecycle revisits page preparation
+- **WHEN** Wicket visits the same entity page preparation path more than once during one rendering request
+- **THEN** the framework invokes each matching view primer at most once for that entity page during that request
+
+#### Scenario: Root logical type has no registered primer
+- **WHEN** Wicket renders an entity page whose exact root logical type has no view-primer registration
+- **THEN** the framework prepares and renders the page normally without executing an application priming callback
+
+### Requirement: Multiple matching primers all execute
+The registry SHALL permit multiple action or view primers for the same exact key and the framework SHALL invoke each matching callback deterministically.
+Primer implementations MUST be independently idempotent and MUST NOT depend on relative callback order.
+
+#### Scenario: Several primers match one key
+- **WHEN** two independently registered primers match one action or view lookup key
+- **THEN** the framework invokes both primers exactly once for that priming opportunity
+
+### Requirement: Primer execution is synchronous and failure-transparent
+Action and view primers MUST be synchronous, read-only application callbacks intended to load likely-needed objects into the active ORM persistence context.
+The framework SHALL propagate any callback exception through the enclosing action execution or Wicket request and SHALL NOT retry, suppress, or convert the failure into an optional optimization result.
+
+#### Scenario: Action primer fails
+- **WHEN** a matching action primer throws an exception
+- **THEN** the action method is not invoked and the exception propagates through the action execution
+
+#### Scenario: View primer fails
+- **WHEN** a matching view primer throws an exception
+- **THEN** page preparation does not continue normally and the exception propagates through the Wicket request
diff --git a/openspec/changes/add-application-priming-hooks/tasks.md b/openspec/changes/add-application-priming-hooks/tasks.md
new file mode 100644
index 00000000000..c3a9782f617
--- /dev/null
+++ b/openspec/changes/add-application-priming-hooks/tasks.md
@@ -0,0 +1,33 @@
+## 1. Public priming contracts
+
+- [ ] 1.1 Add applib contracts for `PrimingRegistrar`, startup-only `PrimingRegistry`, typed `ActionPrimer`, and typed `ViewPrimer`.
+- [ ] 1.2 Implement the immutable `ActionArguments` API with arity, indexed access, type-checked access, and an unmodifiable list view.
+- [ ] 1.3 Add API tests and Javadocs covering array-valued parameters, null values, incompatible typed access, and mutation rejection.
+
+## 2. Metamodel-backed registry
+
+- [ ] 2.1 Implement the registry service and initialize it from all application registrars after metamodel loading.
+- [ ] 2.2 Resolve action keys from an exact domain class plus local action name, including domain-facing contributed-action identities.
+- [ ] 2.3 Resolve view keys from the exact logical type of the registered domain class without hierarchy traversal.
+- [ ] 2.4 Validate unknown classes, missing logical types, malformed or unknown action names, and post-freeze mutation with actionable startup failures.
+- [ ] 2.5 Freeze registrations into immutable exact-match multimaps that retain every callback registered for a key.
+- [ ] 2.6 Add registry tests for valid singular and plural registration, contributed actions, multiple callbacks, deterministic dispatch, exact type matching, invalid registration, and freezing.
+
+## 3. Action execution integration
+
+- [ ] 3.1 Invoke matching action primers after validation and executing-phase argument adjustment but immediately before ordinary domain method invocation.
+- [ ] 3.2 Supply the domain-facing owner or mixee, effective immutable arguments, and the current interaction's persistence context to each callback.
+- [ ] 3.3 Exclude pass-through invocation and synthetic mixed-in property or collection access while retaining ordinary nested and wrapper-mediated action priming.
+- [ ] 3.4 Add action-execution tests for timing, effective arguments, contributed-action targets, nested execution, multiple callbacks, no-registration behavior, and propagated failures.
+
+## 4. Wicket entity-page integration
+
+- [ ] 4.1 Invoke matching view primers for authorized entity pages before expensive title, layout, property, and collection preparation.
+- [ ] 4.2 Add request-local guarding so each matching primer runs at most once per entity page in one rendering request and can run again in a later full or Ajax request.
+- [ ] 4.3 Add Wicket lifecycle tests for initial rendering, subsequent rendering requests, exact root logical-type selection, no-registration behavior, callback ordering relative to member access, and propagated failures.
+
+## 5. Documentation and verification
+
+- [ ] 5.1 Document registrar setup, action and view examples, exact metamodel matching, callback lifecycle, and the read-only, synchronous, idempotent, and thread-safe callback contract.
+- [ ] 5.2 Run focused applib, metamodel, runtime-services, and Wicket test suites and resolve regressions.
+- [ ] 5.3 Run strict OpenSpec validation for `add-application-priming-hooks` and confirm the change is apply-ready.