@cratis/pi 2.24.1 → 2.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,367 @@
1
+ ---
2
+ name: cratis-arc-command-kotlin
3
+ description: Define a Cratis Arc command in Kotlin or Java — the @Command class, its handle() method, the optional provide() step, Chronicle event responses, and command authorization. Use when adding a command to a Kotlin or Java Arc application, choosing what handle() should return, or wiring Chronicle event append from a command. Do not use for the .NET Arc command shape, for validation-only changes, or for query/read-model definition.
4
+ license: MIT
5
+ ---
6
+
7
+ # Define an Arc command in Kotlin or Java
8
+
9
+ Arc.Kotlin brings the same command shape as Arc .NET to the JVM: a class carries
10
+ the user's intent and owns its own handler. KSP (the Kotlin Symbol Processor)
11
+ discovers it at compile time and generates a reflection-free `CommandHandler` —
12
+ there is no controller or handler class to write by hand.
13
+
14
+ ## Verified product sources
15
+
16
+ Verified against `Arc.Kotlin` at tag **`v7.3.0`**, re-verified at commit
17
+ `d0aa6a4` (14 commits past the tag), against the documented consumer setup:
18
+ JDK 17, Gradle 8.14.4, Kotlin 2.4.20, KSP 2.3.12, Spring Boot 4.1.x. The
19
+ re-verification picked up a real behavior correction to authorization
20
+ override — see "Protect the command" below — confirmed against
21
+ `ArcSymbolProcessorAuthorizationPrecedenceCompilationTest` and
22
+ `buildAuthorization` in `ArcSymbolProcessor.kt`, not documentation prose alone:
23
+ two of Arc.Kotlin's own reference pages (`reference/annotations.md`,
24
+ `troubleshooting.md`) are currently stale on this point relative to
25
+ `guides/security.md`, `reference/parity.md`, and the compiler source. The local workspace version is `0.0.0-SNAPSHOT`; use a released version
26
+ from Maven Central for a real application.
27
+
28
+ | Artifact | Purpose |
29
+ | --- | --- |
30
+ | `io.cratis:arc` | `io.cratis.arc.artifacts.Command`, `CommandKey`, `FromServices` |
31
+ | `io.cratis:arc-ksp` | the KSP processor that generates the command handler |
32
+ | `io.cratis:arc-spring-boot-starter` | Spring Boot auto-configuration, the command pipeline host |
33
+ | `io.cratis:arc-chronicle-spring-boot-starter` | optional — Chronicle event append from a returned event |
34
+ | `io.cratis.arc` Gradle plugin | applies Kotlin/JVM + KSP, adds the dependencies, configures the module name and proxy output |
35
+
36
+ Arc.Kotlin has no Chronicle dependency by default. Add the optional Chronicle
37
+ starter only when a command should append a returned event or read a Chronicle
38
+ read model; without it, `handle()` returns an ordinary response value.
39
+
40
+ Java applications still need Kotlin and KSP on the build — Arc generates Kotlin
41
+ adapters for Java declarations even in an all-Java application. A Java project
42
+ uses the identical Gradle plugin and `cratisArc {}` block below; only the
43
+ application source files differ (Java records instead of Kotlin data classes).
44
+
45
+ ## Set up the project
46
+
47
+ ```kotlin
48
+ // settings.gradle.kts
49
+ pluginManagement {
50
+ repositories {
51
+ mavenCentral()
52
+ gradlePluginPortal()
53
+ }
54
+ }
55
+ ```
56
+
57
+ ```kotlin
58
+ // build.gradle.kts
59
+ plugins {
60
+ id("io.cratis.arc") version "<version>"
61
+ kotlin("plugin.spring") version "2.4.20"
62
+ id("org.springframework.boot") version "4.1.1"
63
+ id("io.spring.dependency-management") version "1.1.7"
64
+ }
65
+
66
+ repositories {
67
+ mavenCentral()
68
+ }
69
+
70
+ cratisArc {
71
+ moduleName.set("<ModuleName>")
72
+ dependencyVersion.set("<version>")
73
+ endpoints {
74
+ segmentsToSkip.set(<packageSegmentsUnderModuleRoot>)
75
+ }
76
+ }
77
+
78
+ dependencies {
79
+ implementation("io.cratis:arc-spring-boot-starter:<version>")
80
+ implementation("org.springframework.boot:spring-boot-starter-webmvc")
81
+ }
82
+ ```
83
+
84
+ The plugin applies Kotlin/JVM and KSP, targets JDK 17, and treats warnings as
85
+ errors. `moduleName` names the generated artifact module and manifest; every
86
+ module in an application needs a distinct one. `endpoints.segmentsToSkip` must
87
+ match `cratis.arc.endpoints.segments-to-skip-for-route` in
88
+ `application.properties` — the Spring Boot starter fails startup with a named
89
+ mismatch when proxies were generated and the two settings disagree.
90
+
91
+ ## Declare the command
92
+
93
+ ```kotlin
94
+ package example.tasks
95
+
96
+ import io.cratis.arc.artifacts.Command
97
+ import io.cratis.arc.artifacts.CommandKey
98
+ import io.cratis.chronicle.events.EventType
99
+
100
+ @EventType
101
+ data class TaskCreated(val title: String)
102
+
103
+ @Command
104
+ data class CreateTask(@CommandKey val id: String, val title: String) {
105
+ fun handle(): TaskCreated = TaskCreated(title)
106
+ }
107
+ ```
108
+
109
+ ```java
110
+ package example.tasks;
111
+
112
+ import io.cratis.arc.artifacts.Command;
113
+ import java.util.concurrent.CompletableFuture;
114
+ import java.util.concurrent.CompletionStage;
115
+
116
+ @Command
117
+ public record CreateTask(String title) {
118
+ public CompletionStage<TaskCreated> handle(TaskRepository repository) {
119
+ var task = repository.create(title);
120
+ return CompletableFuture.completedFuture(new TaskCreated(task.id(), task.title()));
121
+ }
122
+ }
123
+ ```
124
+
125
+ Rules the framework actually enforces:
126
+
127
+ - `@Command` (`io.cratis.arc.artifacts.Command`) targets a class — a Kotlin
128
+ class or data class, or a Java record. The type must also declare a public
129
+ instance `handle` method; KSP reports `ARCKSP0100` when a type looks
130
+ command-like but lacks `@Command`.
131
+ - Only top-level **public** classes qualify. Kotlin's implicit-public
132
+ visibility counts — spelling `public` is style, not a requirement — but
133
+ private, internal, and Java package-private classes fail closed
134
+ (`ARCKSP0101`/`ARCKSP0102`).
135
+ - `handle` resolves unannotated parameters from Spring by type, exactly like a
136
+ constructor-injected `@Service`. There is no separate DI container to wire.
137
+ - `handle` may return `Unit`/`void`, a plain response value, a suspend result
138
+ (Kotlin), or `CompletionStage<T>` (Java). The generated adapter runs inside
139
+ Arc's bounded application coroutine scope — never `GlobalScope`, never
140
+ thread-local request state.
141
+ - Name the command as the action — `CreateTask`, not `CreateTaskCommand`.
142
+ - KSP keeps constructor properties in declaration order, then appends public
143
+ declared body properties in name order. A body `@CommandKey`, `@Valid`, and
144
+ field/getter validation constraints are retained the same way constructor
145
+ properties are; a constructor key plus a body key fails with `ARCKSP0106`.
146
+ - Mark server-only state `@field:JsonIgnore` or `@get:JsonIgnore`, or keep it
147
+ nonpublic. A computed or read-only property reached through command input
148
+ fails with `ARCKSP0300` — use a backed property, ignore the member, or
149
+ return a separate output model instead.
150
+
151
+ ## Fetch what the handler needs with `provide()`
152
+
153
+ `provide` is an optional public instance method that runs **after**
154
+ authorization and validation but **before** `handle`. `POST
155
+ <command-route>/validate` never invokes it. Its parameters resolve from Spring,
156
+ exactly like `handle`'s.
157
+
158
+ ```kotlin
159
+ @Command
160
+ data class CompleteTask(val taskId: TaskId) {
161
+ suspend fun provide(tasks: Tasks): Task = tasks.get(taskId)
162
+
163
+ fun handle(task: Task, audit: AuditLog): TaskCompleted {
164
+ audit.record(task.id)
165
+ return TaskCompleted(task.id)
166
+ }
167
+ }
168
+ ```
169
+
170
+ - Return one value, a `Pair`, a `Triple`, `CommandProvidedValues`, or an
171
+ `ArcOneOf` alternative. Provided values are matched to `handle`'s parameters
172
+ in declaration order; each match is consumed once, and an unmatched
173
+ parameter falls back to Spring.
174
+ - A returned `CommandResult`, `ValidationResult`, a non-empty validation-result
175
+ collection, or `AuthorizationResult` is a control signal — it short-circuits
176
+ `handle` instead of feeding it. This is how a `provide` rejects a request the
177
+ user can act on; do not throw for that.
178
+ - Java retains `CommandProvidedValues.of(...)` / `CommandResponseValues.of(...)`
179
+ factories; Kotlin has `commandProvidedValuesOf(...)` /
180
+ `commandResponseValuesOf(...)` helpers for explicit ordered aggregates.
181
+
182
+ ## Return a Chronicle event
183
+
184
+ Requires the optional `io.cratis:arc-chronicle-spring-boot-starter`. A plain
185
+ event response needs a stable command key backed by `String`, `UUID`, a
186
+ number, or a concept wrapping one of those — mark it `@CommandKey`.
187
+
188
+ ```kotlin
189
+ @Command
190
+ data class CreateTask(@CommandKey val id: String, val title: String) {
191
+ fun handle(): TaskCreated = TaskCreated(title)
192
+ }
193
+ ```
194
+
195
+ Without a usable `@CommandKey`, Arc returns an error validation result
196
+ (`reason: "rule"`, `reasonDetail: "commandKey"`) rather than guessing an
197
+ event-source identifier.
198
+
199
+ | Return | What Arc does |
200
+ | --- | --- |
201
+ | A single `@EventType` value | Appended to the command key |
202
+ | A non-empty collection/array of `@EventType` values | All appended to the command key, atomically |
203
+ | `EventForEventSourceId(id, event)` | Appended to `id` instead — the cross-stream write |
204
+ | A collection mixing plain events and `EventForEventSourceId` | Plain events use the command key, wrapped events keep their own id; appended atomically through the cross-stream path |
205
+ | `EventsWithConcurrencyScopes` | Routed events only, with explicit per-source-id concurrency scopes |
206
+ | Anything else, or a value that mixes an event with a non-event | Becomes (or corrupts) the response — a mixed response containing a non-`@EventType` value fails before anything is appended |
207
+
208
+ ```kotlin
209
+ import io.cratis.arc.chronicle.eventsWithConcurrencyScopes
210
+
211
+ fun handle(): EventsWithConcurrencyScopes = eventsWithConcurrencyScopes {
212
+ event("account-42", FundsWithdrawn(100))
213
+ event("ledger-2025", LedgerEntryAdded("account-42", 100))
214
+ concurrencyScope("account-42") {
215
+ withEventSourceId()
216
+ }
217
+ }
218
+ ```
219
+
220
+ Java builds the same shape with `EventsWithConcurrencyScopes.builder()` and
221
+ fluent `event(...)`.
222
+
223
+ Declare stable Chronicle routing defaults on the command when its events share
224
+ them, rather than repeating metadata on every `EventForEventSourceId`:
225
+
226
+ ```kotlin
227
+ @Command
228
+ @CommandEventSourceType("Task")
229
+ @CommandEventStreamType("Tasks")
230
+ @CommandEventStreamId("active")
231
+ @CommandEventSubject("task-owner")
232
+ data class CreateTask(@CommandKey val id: String, val title: String) {
233
+ fun handle(): TaskCreated = TaskCreated(title)
234
+ }
235
+ ```
236
+
237
+ Each annotation is optional and must be nonblank with no control characters
238
+ when present (`ARCKSP0110` otherwise). Omit one to keep Chronicle's own
239
+ fallback: source/stream type default to `Default`, stream id/subject default
240
+ to the event-source identifier. An explicit `EventForEventSourceId` value keeps
241
+ its own metadata; only its missing fields inherit the command's declaration.
242
+ Implement `CommandEventStreamIdProvider` or `CommandEventSubjectProvider`
243
+ instead when a value depends on the command instance — never combine a
244
+ provider with the matching static annotation (`ARCKSP0110`).
245
+
246
+ A returned event, a mixed collection, or `EventsWithConcurrencyScopes` commits
247
+ through one staged transaction per outermost command-execution root: any child
248
+ or root failure discards every staged event, and a successful root appends
249
+ once with `appendMany`. Constraint violations surface as a validation result
250
+ with `reason: "constraintViolation"`; concurrency failures surface as
251
+ `concurrencyViolation` with expected/actual sequence numbers.
252
+
253
+ ## Inject a Chronicle read model into the handler
254
+
255
+ When `ChronicleOptions` lists a read-model artifact, `handle` (or `provide`)
256
+ can request the **current** instance for the command's own resolved key as an
257
+ unannotated parameter — resolution uses the generated command key and the
258
+ captured tenant store:
259
+
260
+ ```kotlin
261
+ @Command
262
+ data class Withdraw(@CommandKey val accountId: String, val amount: Int) {
263
+ fun handle(balance: AccountBalance): FundsWithdrawn {
264
+ require(balance.available >= amount)
265
+ return FundsWithdrawn(amount)
266
+ }
267
+ }
268
+ ```
269
+
270
+ A value from `provide` still takes precedence over this injection. A missing
271
+ model leaves the dependency unresolved rather than silently falling through to
272
+ another store; an invalid key or unavailable store fails the command.
273
+
274
+ ## Protect the command
275
+
276
+ ```kotlin
277
+ @Command
278
+ @Authorize(roles = ["admin"])
279
+ data class DeactivateAccount(@CommandKey val accountId: String) {
280
+ fun handle(): AccountDeactivated = AccountDeactivated()
281
+ }
282
+ ```
283
+
284
+ Use `@AllowAnonymous`, `@Authorize(policy = "...")`, `@Authorize(roles = [...])`,
285
+ or repeatable `@Roles`. **The operation replaces the class wholesale, in
286
+ either direction — this matches Arc .NET's `AuthorizationEvaluator.IsAuthorized(MethodInfo)`,
287
+ which resolves the method's own metadata first and falls back to the
288
+ declaring type only when the method declares none.** When `handle` declares
289
+ any authorization metadata of its own, only *its* policy/roles/schemes/anonymity
290
+ apply and the class declaration is discarded entirely — so an operation can
291
+ both narrow (a `@Roles("auditor")` operation on a class requiring `admin`
292
+ admits auditors and rejects admins) and *widen* access (an `@AllowAnonymous`
293
+ operation on an `@Authorize`-protected class is reachable without
294
+ authentication — the shape a login-screen query needs on an otherwise
295
+ protected read model):
296
+
297
+ ```kotlin
298
+ @ReadModel
299
+ @Authorize
300
+ data class AuthenticationQueryItem(val message: String) {
301
+ companion object {
302
+ // Overrides the class: anyone may subscribe to this one.
303
+ @JvmStatic
304
+ @AllowAnonymous
305
+ fun anonymous(@FromServices source: AuthenticationQuerySource): Flow<AuthenticationQueryItem> =
306
+ source.observeAnonymous()
307
+ }
308
+ }
309
+ ```
310
+
311
+ Combining `@AllowAnonymous` with `@Authorize`/`@Roles` is only ever a
312
+ contradiction — and only ever fails with `ARCKSP0108` — **on the exact same
313
+ declaration** (both on the class, or both on the operation). Splitting them
314
+ across a class and its operation is not a conflict; it is the override
315
+ mechanism above.
316
+
317
+ ## Generate the TypeScript proxy
318
+
319
+ ```kotlin
320
+ cratisArc {
321
+ moduleName.set("<ModuleName>")
322
+ dependencyVersion.set("<version>")
323
+ proxies {
324
+ outputDirectory.set(layout.buildDirectory.dir("generated/arc-proxies"))
325
+ segmentsToSkip.set(<packageSegmentsUnderModuleRoot>)
326
+ }
327
+ }
328
+ ```
329
+
330
+ An unset `proxies.outputDirectory` skips generation entirely. `proxies.segmentsToSkip`
331
+ controls the generated **file path**; the separate `endpoints.segmentsToSkip`
332
+ (and its runtime twin `cratis.arc.endpoints.segments-to-skip-for-route`)
333
+ controls the **HTTP route**. Mixing the two up produces the wrong file layout
334
+ or the wrong route, not a startup error — they are easy to confuse because
335
+ they read almost identically. The generated proxy's runtime contract
336
+ (`route`, `execute()`, `validate()`, `CommandResult`) is the same
337
+ `@cratis/arc` client shape as Arc .NET; see the `cratis-arc-command` skill for
338
+ that contract.
339
+
340
+ ## Route near misses
341
+
342
+ - Adding or changing a rejection rule on an existing command:
343
+ `cratis-arc-validation-kotlin`.
344
+ - Defining or changing a `@ReadModel` query: `cratis-arc-query-kotlin`.
345
+ - Talking to Chronicle outside a command (a plain worker, a reactor's own
346
+ event types, direct append): `cratis-chronicle-client-kotlin`.
347
+ - The C# Arc command shape and its causation-chain rules: `cratis-arc-command`.
348
+
349
+ ## Verify
350
+
351
+ - The command is a public top-level class or Java record, carries `@Command`,
352
+ and declares a public instance `handle`.
353
+ - Every returned event type carries `@EventType`, and a plain event response
354
+ has a usable `@CommandKey`.
355
+ - Cross-stream events are wrapped in `EventForEventSourceId`; a mixed
356
+ collection contains only events and wrappers, nothing else.
357
+ - Rejections from `provide` or a validator are `ValidationResult`s, never
358
+ thrown exceptions the user is meant to act on.
359
+ - `@AllowAnonymous` is not combined with `@Authorize`/`@Roles` on the exact
360
+ same declaration (class+class or operation+operation) — splitting them
361
+ across a class and its operation is the intended override, not an error.
362
+ - `endpoints.segmentsToSkip` (build) and `segments-to-skip-for-route` (runtime
363
+ properties) agree; the application starts without the mismatch error.
364
+ - `./gradlew build` is clean with zero warnings and zero errors (Kotlin
365
+ `allWarningsAsErrors`, Java `-Xlint:all -Werror`).
366
+ - The generated proxy exists at the configured output directory when one was
367
+ configured, and the frontend compiles against it.
@@ -0,0 +1,225 @@
1
+ ---
2
+ name: cratis-arc-query-kotlin
3
+ description: Add a query to a Cratis Arc @ReadModel in Kotlin or Java — companion-object or static query methods, GET vs RFC QUERY, and observable queries returning Kotlin Flow, JDK Flow.Publisher, or RxJava 3 Observable over SSE and WebSocket. Use when exposing read-model data from a Kotlin or Java Arc application. Do not use for the .NET Arc query shape, for command definition, or for how events populate the read model (Chronicle projections/reducers).
4
+ license: MIT
5
+ ---
6
+
7
+ # Add a query to an Arc read model in Kotlin or Java
8
+
9
+ A `@ReadModel` is a plain Kotlin or Java type whose companion-object (Kotlin) or
10
+ static (Java) methods are queries. KSP generates a reflection-free
11
+ `QueryPerformer` per method; there is no controller to write.
12
+
13
+ ## Verified product sources
14
+
15
+ Verified against `Arc.Kotlin` at tag **`v7.3.0`**, re-verified at commit
16
+ `d0aa6a4` (14 commits past the tag), documented consumer setup: JDK 17,
17
+ Kotlin 2.4.20, KSP 2.3.12, Spring Boot 4.1.x. `io.cratis:arc` supplies
18
+ `io.cratis.arc.artifacts.ReadModel`, `io.cratis.arc.queries.Path`,
19
+ `@QueryHttpMethod`, `@QueryTransport`, `ObservableState<T>`. The optional
20
+ `arc-rxjava3` artifact adds RxJava 3 `Observable` support at runtime. The
21
+ re-verification added `ObservableState<T>` (new since the tag) and the
22
+ `observable-queries.allowed-origins` WebSocket setting — both covered below.
23
+
24
+ ## Declare a one-shot query
25
+
26
+ ```kotlin
27
+ @ReadModel
28
+ @AllowAnonymous
29
+ data class TaskView(val id: String, val title: String) {
30
+ companion object {
31
+ @JvmStatic
32
+ @Path("/api/tasks/by-id")
33
+ suspend fun byId(id: String, @FromServices repository: TaskRepository): TaskView? =
34
+ repository.byId(id)
35
+
36
+ @JvmStatic
37
+ @Path("/api/tasks")
38
+ fun all(@FromServices repository: TaskRepository): List<TaskView> = repository.all()
39
+ }
40
+ }
41
+ ```
42
+
43
+ ```java
44
+ @ReadModel
45
+ @AllowAnonymous
46
+ public record TaskView(String id, String title) {
47
+ @Path("/api/tasks")
48
+ public static List<TaskView> all(@FromServices TaskRepository repository) {
49
+ return repository.all();
50
+ }
51
+ }
52
+ ```
53
+
54
+ Rules the framework enforces:
55
+
56
+ - Query methods live on the read model's Kotlin **companion object** or as
57
+ Java **static** methods. A custom instance method on a `@ReadModel` fails
58
+ compilation.
59
+ - Mark dependency parameters `@FromServices`; caller-supplied arguments stay
60
+ unannotated. A query may also declare exact non-null `QueryRequest` and
61
+ `QueryContext` parameters in any position — KSP resolves service, request,
62
+ and context parameters from the execution context while preserving
63
+ declaration order.
64
+ - Java query methods return a value or `CompletionStage<T>`; generated
65
+ performers await it inside Arc's request coroutine context.
66
+ - Kotlin caller parameters may declare defaults. GET/QUERY/observable
67
+ subscriptions preserve *omission* (leaving the argument out of
68
+ `QueryRequest.arguments`) so the generated performer runs the Kotlin default
69
+ expression; an explicit JSON/subscription `null` is a *supplied* value, not
70
+ omission, and is accepted only by a nullable parameter. Java query
71
+ parameters have no default feature, and overloaded query methods are
72
+ unsupported in either language.
73
+ - Only public, non-generic, non-overloaded methods whose return type contains
74
+ the enclosing read-model shape are treated as queries; adding `@Path`,
75
+ `@QueryHttpMethod`, or `@QueryTransport` explicitly marks a method as a
76
+ query, so an invalid annotated return still fails compilation.
77
+
78
+ ## Choose a route and method
79
+
80
+ Without `@Path`, Arc derives a route from `cratis.arc.endpoints` settings
81
+ (prefix, skipped package segments, kebab case, artifact name). `@Path`
82
+ preserves what you write verbatim.
83
+
84
+ | Transport | When | Shape |
85
+ | --- | --- | --- |
86
+ | `GET` | scalar arguments | `?id=123` |
87
+ | `QUERY` (RFC 9110) | structured arguments, paging, sorting; enabled by `enable-query-http-method` | JSON body `{arguments, paging, sorting}` |
88
+
89
+ ```bash
90
+ curl -sS 'http://localhost:8080/api/tasks/by-id?id=123'
91
+
92
+ curl -sS -X QUERY http://localhost:8080/api/tasks \
93
+ -H 'Content-Type: application/json' \
94
+ -d '{"arguments":{},"paging":{"page":0,"pageSize":25},"sorting":{"field":"title","direction":"ascending"}}'
95
+ ```
96
+
97
+ GET reserves `page`, `pageSize`, `sortBy`, `sortDirection`. Client argument
98
+ names match case-insensitively for GET and QUERY. QUERY responses always carry
99
+ `Cache-Control: no-store`. Prefer GET for a query with `DateOnly`/`TimeOnly`
100
+ parameters — the pinned `@cratis/arc` client's QUERY body serializes those
101
+ component objects with native `JSON.stringify` rather than their typed
102
+ serializer, which GET does not go through.
103
+
104
+ `@QueryHttpMethod(GET|QUERY|AUTO)` sets the generated proxy's preference. Put
105
+ it on the `@ReadModel` class for a default, and override per method.
106
+
107
+ ## Declare an observable query
108
+
109
+ Return one of three streaming families from a companion/static method; KSP
110
+ generates an `OBSERVABLE` performer automatically — no `@QueryTransport`
111
+ needed for these:
112
+
113
+ - Kotlin `kotlinx.coroutines.flow.Flow<T>` / `Flow<List<T>>`
114
+ - JDK `java.util.concurrent.Flow.Publisher<T>` / `Publisher<List<T>>`
115
+ - RxJava 3 `Observable<T>` / `ObservableSource<T>` / `Subject<T>` — requires
116
+ the optional `arc-rxjava3` artifact at runtime
117
+
118
+ A one-shot return (`T`, `List<T>`, `Page<T>`) stays request-response.
119
+
120
+ ```kotlin
121
+ @ReadModel
122
+ @AllowAnonymous
123
+ data class TaskView(val id: String, val title: String) {
124
+ companion object {
125
+ @JvmStatic
126
+ @Path("/api/tasks/watch")
127
+ fun watch(@FromServices repository: TaskRepository): Flow<List<TaskView>> =
128
+ repository.watchAll()
129
+ }
130
+ }
131
+ ```
132
+
133
+ ### Give a Java query a current value to answer GET with
134
+
135
+ A plain `SubmissionPublisher` emits but holds nothing — nobody subscribing
136
+ learns the current value until the next change, and a snapshot `GET` against
137
+ it comes back `202 Not Ready` forever if nothing has happened yet. Kotlin has
138
+ `MutableStateFlow` for this; the JDK has no equivalent, so Arc supplies
139
+ `ObservableState<T>`:
140
+
141
+ ```java
142
+ @Component
143
+ public final class TaskSource {
144
+ private final ObservableState<List<TaskView>> tasks = new ObservableState<>(List.of());
145
+
146
+ public Flow.Publisher<List<TaskView>> observe() {
147
+ return tasks;
148
+ }
149
+
150
+ public void publish(List<TaskView> updated) {
151
+ tasks.set(updated);
152
+ }
153
+ }
154
+ ```
155
+
156
+ `ObservableState<T>` is a `Flow.Publisher<T>`, so a query method returns it
157
+ directly. Every subscriber sees the current value first and then each change;
158
+ a subscriber that falls behind sees only the newest value, not a backlog —
159
+ the right behavior for state rather than a stream of discrete events. Reach
160
+ for it whenever a **Java** observable query should also answer a plain `GET`.
161
+ Kotlin code has no reason to — return `MutableStateFlow` as a `Flow` instead.
162
+
163
+ ### Consuming it
164
+
165
+ The same generated route serves three direct transports:
166
+
167
+ | Transport | Selected by | Behavior |
168
+ | --- | --- | --- |
169
+ | HTTP GET/QUERY | ordinary request | `200` with the current value when the source is a `StateFlow` (already holds one); a cold `Flow`/`Publisher` returns `202` (not ready) unless `waitForFirstResult=true`, optionally bounded by `waitForFirstResultTimeout=<seconds>` |
170
+ | SSE | `Accept: text/event-stream` | each result as `data: {QueryResult}\n\n` |
171
+ | WebSocket | upgrading the query route | `{"type":"Data","data":{QueryResult}}` frames, plus `Ping`/`Pong` |
172
+
173
+ Multiple subscriptions can multiplex over one physical connection through
174
+ `/.cratis/queries/ws`, or `/.cratis/queries/sse` with POST
175
+ `/.cratis/queries/sse/subscribe` / `/unsubscribe`. Every subscription still
176
+ passes through its own query's authorization — a protected query terminates
177
+ `Unauthorized` on that subscription without disturbing others sharing the
178
+ connection.
179
+
180
+ Set `transferMode=delta` to get the full first snapshot followed by change
181
+ sets only; `transferMode=full` sends every result as a full snapshot with no
182
+ change set. Omitting it keeps the legacy behavior of always sending both. With
183
+ `delta`, Arc prefers a stable item identity; without one it falls back to
184
+ serialized-JSON identity and reports only additions/removals (a changed field
185
+ is a removal plus an addition, never a replacement) — change sets never encode
186
+ reordering.
187
+
188
+ ## Configure the WebSocket origin for a separate dev server
189
+
190
+ Spring answers a cross-origin WebSocket handshake with `403`, and a browser
191
+ reports that as a socket that silently never opens rather than as a visible
192
+ error. Serving the frontend from a dev server on another port (Vite, webpack
193
+ dev server) makes every observable-query WebSocket handshake cross-origin.
194
+ Set the allowed origin explicitly:
195
+
196
+ ```properties
197
+ cratis.arc.observable-queries.allowed-origins=http://localhost:5173
198
+ ```
199
+
200
+ Leave it empty in a real deployment — the WebSocket handshake is not subject
201
+ to the same-origin policy `fetch` obeys, so this setting is what stops another
202
+ site from opening a socket carrying your visitor's cookies.
203
+
204
+ ## Route near misses
205
+
206
+ - Defining or changing a `@Command`: `cratis-arc-command-kotlin`.
207
+ - Adding a query-argument rejection rule: `cratis-arc-validation-kotlin`.
208
+ - How events populate the read model in the first place (Chronicle projections
209
+ and reducers): `cratis-chronicle-client-kotlin`, `cratis-chronicle-projection`,
210
+ `cratis-chronicle-reducer`.
211
+ - Server-side paging semantics shared with Arc .NET: `cratis-arc-query-paging`.
212
+ - The C# Arc query/read-model shape: the vertical-slices and Arc React skills.
213
+
214
+ ## Verify
215
+
216
+ - Query methods live on the companion object (Kotlin) or as static methods
217
+ (Java), are public, non-generic, and not overloaded.
218
+ - Every dependency parameter is `@FromServices`; caller arguments are not.
219
+ - `@Path`, if declared, matches the route the frontend expects; otherwise the
220
+ conventional route (prefix, skipped segments, kebab case) is understood.
221
+ - An observable query's return type is one of the three recognized streaming
222
+ families, and the RxJava artifact is present when `Observable` is used.
223
+ - A `delta`-mode observable query has a stable item identity, not incidental
224
+ reliance on serialized-JSON fallback identity.
225
+ - `./gradlew build` is clean with zero warnings and zero errors.