@spine-event-engine/server 2.0.0-snapshot.2 → 2.0.0-snapshot.21

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.
Files changed (227) hide show
  1. package/README.md +225 -518
  2. package/REFERENCE.md +498 -49
  3. package/dist/browser/browser-server.d.ts +95 -0
  4. package/dist/browser/browser-server.d.ts.map +1 -0
  5. package/dist/{server → browser}/browser-server.js +93 -7
  6. package/dist/browser/browser-server.js.map +1 -0
  7. package/dist/browser/durable-subscription-bindings.d.ts.map +1 -0
  8. package/dist/browser/durable-subscription-bindings.js.map +1 -0
  9. package/dist/browser/index.d.ts +12 -0
  10. package/dist/browser/index.d.ts.map +1 -0
  11. package/dist/browser/index.js +24 -0
  12. package/dist/browser/index.js.map +1 -0
  13. package/dist/browser/options.d.ts +183 -0
  14. package/dist/browser/options.d.ts.map +1 -0
  15. package/dist/browser/options.js +15 -0
  16. package/dist/browser/options.js.map +1 -0
  17. package/dist/bus/command-bus.d.ts +2 -0
  18. package/dist/bus/command-bus.d.ts.map +1 -1
  19. package/dist/bus/command-bus.js +36 -1
  20. package/dist/bus/command-bus.js.map +1 -1
  21. package/dist/bus/event-dispatcher-registry.d.ts +2 -2
  22. package/dist/bus/event-dispatcher-registry.js +3 -3
  23. package/dist/bus/event-dispatcher-registry.js.map +1 -1
  24. package/dist/context/bounded-context.d.ts +238 -83
  25. package/dist/context/bounded-context.d.ts.map +1 -1
  26. package/dist/context/bounded-context.js +1083 -286
  27. package/dist/context/bounded-context.js.map +1 -1
  28. package/dist/context/effective-tenant.d.ts +9 -0
  29. package/dist/context/effective-tenant.d.ts.map +1 -0
  30. package/dist/context/effective-tenant.js +58 -0
  31. package/dist/context/effective-tenant.js.map +1 -0
  32. package/dist/context/entity-inbox.d.ts.map +1 -1
  33. package/dist/context/entity-inbox.js +24 -17
  34. package/dist/context/entity-inbox.js.map +1 -1
  35. package/dist/context/local-inbox-handoff.d.ts +7 -3
  36. package/dist/context/local-inbox-handoff.d.ts.map +1 -1
  37. package/dist/context/local-inbox-handoff.js +32 -26
  38. package/dist/context/local-inbox-handoff.js.map +1 -1
  39. package/dist/context/projection-handoff.d.ts.map +1 -1
  40. package/dist/context/projection-handoff.js +36 -31
  41. package/dist/context/projection-handoff.js.map +1 -1
  42. package/dist/context/tenant-index.d.ts.map +1 -1
  43. package/dist/context/tenant-index.js +72 -2
  44. package/dist/context/tenant-index.js.map +1 -1
  45. package/dist/delivery/delivery-ports.d.ts +14 -4
  46. package/dist/delivery/delivery-ports.d.ts.map +1 -1
  47. package/dist/delivery/delivery.d.ts +19 -21
  48. package/dist/delivery/delivery.d.ts.map +1 -1
  49. package/dist/delivery/delivery.js +541 -158
  50. package/dist/delivery/delivery.js.map +1 -1
  51. package/dist/delivery/inbox-storage.d.ts +12 -3
  52. package/dist/delivery/inbox-storage.d.ts.map +1 -1
  53. package/dist/delivery/inbox-storage.js +160 -67
  54. package/dist/delivery/inbox-storage.js.map +1 -1
  55. package/dist/delivery/inbox.d.ts +9 -0
  56. package/dist/delivery/inbox.d.ts.map +1 -1
  57. package/dist/delivery/inbox.js +11 -0
  58. package/dist/delivery/inbox.js.map +1 -1
  59. package/dist/delivery/sharded-work-registry.d.ts.map +1 -1
  60. package/dist/delivery/sharded-work-registry.js +2 -1
  61. package/dist/delivery/sharded-work-registry.js.map +1 -1
  62. package/dist/entity/entity-metadata.d.ts +14 -5
  63. package/dist/entity/entity-metadata.d.ts.map +1 -1
  64. package/dist/entity/entity-metadata.js +38 -2
  65. package/dist/entity/entity-metadata.js.map +1 -1
  66. package/dist/entity/entity-storage-descriptor.d.ts +25 -5
  67. package/dist/entity/entity-storage-descriptor.d.ts.map +1 -1
  68. package/dist/entity/entity-storage-descriptor.js +69 -23
  69. package/dist/entity/entity-storage-descriptor.js.map +1 -1
  70. package/dist/entity/entity-transaction.d.ts +46 -42
  71. package/dist/entity/entity-transaction.d.ts.map +1 -1
  72. package/dist/entity/entity-transaction.js +130 -35
  73. package/dist/entity/entity-transaction.js.map +1 -1
  74. package/dist/entity/entity.d.ts +243 -98
  75. package/dist/entity/entity.d.ts.map +1 -1
  76. package/dist/entity/entity.js +405 -226
  77. package/dist/entity/entity.js.map +1 -1
  78. package/dist/entity/spec-scanner.d.ts +13 -1
  79. package/dist/entity/spec-scanner.d.ts.map +1 -1
  80. package/dist/entity/spec-scanner.js +20 -3
  81. package/dist/entity/spec-scanner.js.map +1 -1
  82. package/dist/handler/command-registration-readiness.d.ts +6 -6
  83. package/dist/handler/command-registration-readiness.d.ts.map +1 -1
  84. package/dist/handler/command-registration-readiness.js +4 -2
  85. package/dist/handler/command-registration-readiness.js.map +1 -1
  86. package/dist/handler/declared-rejections.d.ts +11 -0
  87. package/dist/handler/declared-rejections.d.ts.map +1 -0
  88. package/dist/handler/declared-rejections.js +34 -0
  89. package/dist/handler/declared-rejections.js.map +1 -0
  90. package/dist/handler/event-registration-readiness.d.ts +13 -56
  91. package/dist/handler/event-registration-readiness.d.ts.map +1 -1
  92. package/dist/handler/event-registration-readiness.js +23 -43
  93. package/dist/handler/event-registration-readiness.js.map +1 -1
  94. package/dist/handler/generated-handler-registry.d.ts +72 -33
  95. package/dist/handler/generated-handler-registry.d.ts.map +1 -1
  96. package/dist/handler/generated-handler-registry.js +421 -43
  97. package/dist/handler/generated-handler-registry.js.map +1 -1
  98. package/dist/handler/generated-registry-discovery.d.ts.map +1 -1
  99. package/dist/handler/generated-registry-discovery.js +2 -4
  100. package/dist/handler/generated-registry-discovery.js.map +1 -1
  101. package/dist/handler/handler-decorators.d.ts +91 -37
  102. package/dist/handler/handler-decorators.d.ts.map +1 -1
  103. package/dist/handler/handler-decorators.js +124 -31
  104. package/dist/handler/handler-decorators.js.map +1 -1
  105. package/dist/handler/handler-metadata.d.ts +105 -60
  106. package/dist/handler/handler-metadata.d.ts.map +1 -1
  107. package/dist/handler/handler-metadata.js +275 -57
  108. package/dist/handler/handler-metadata.js.map +1 -1
  109. package/dist/handler/registration-readiness-metadata.d.ts +4 -0
  110. package/dist/handler/registration-readiness-metadata.d.ts.map +1 -1
  111. package/dist/handler/registration-readiness-metadata.js +84 -2
  112. package/dist/handler/registration-readiness-metadata.js.map +1 -1
  113. package/dist/handler/standalone.d.ts +25 -0
  114. package/dist/handler/standalone.d.ts.map +1 -0
  115. package/dist/handler/standalone.js +38 -0
  116. package/dist/handler/standalone.js.map +1 -0
  117. package/dist/index.d.ts +9 -9
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +3 -3
  120. package/dist/index.js.map +1 -1
  121. package/dist/repository/command-routing.d.ts +63 -10
  122. package/dist/repository/command-routing.d.ts.map +1 -1
  123. package/dist/repository/command-routing.js +41 -8
  124. package/dist/repository/command-routing.js.map +1 -1
  125. package/dist/repository/event-routing.d.ts +64 -9
  126. package/dist/repository/event-routing.d.ts.map +1 -1
  127. package/dist/repository/event-routing.js +42 -7
  128. package/dist/repository/event-routing.js.map +1 -1
  129. package/dist/repository/primitive-id.d.ts +12 -23
  130. package/dist/repository/primitive-id.d.ts.map +1 -1
  131. package/dist/repository/primitive-id.js +2 -27
  132. package/dist/repository/primitive-id.js.map +1 -1
  133. package/dist/repository/repository.d.ts +317 -87
  134. package/dist/repository/repository.d.ts.map +1 -1
  135. package/dist/repository/repository.js +3746 -856
  136. package/dist/repository/repository.js.map +1 -1
  137. package/dist/repository/state-update-routing.d.ts +67 -8
  138. package/dist/repository/state-update-routing.d.ts.map +1 -1
  139. package/dist/repository/state-update-routing.js +45 -6
  140. package/dist/repository/state-update-routing.js.map +1 -1
  141. package/dist/runtime/runtime.js +1 -1
  142. package/dist/runtime/runtime.js.map +1 -1
  143. package/dist/runtime/signal-metadata.d.ts +22 -47
  144. package/dist/runtime/signal-metadata.d.ts.map +1 -1
  145. package/dist/runtime/signal-metadata.js +34 -69
  146. package/dist/runtime/signal-metadata.js.map +1 -1
  147. package/dist/runtime/signal-publisher.d.ts +144 -0
  148. package/dist/runtime/signal-publisher.d.ts.map +1 -0
  149. package/dist/runtime/signal-publisher.js +248 -0
  150. package/dist/runtime/signal-publisher.js.map +1 -0
  151. package/dist/runtime/standalone-handler-runtime.d.ts +130 -0
  152. package/dist/runtime/standalone-handler-runtime.d.ts.map +1 -0
  153. package/dist/runtime/standalone-handler-runtime.js +382 -0
  154. package/dist/runtime/standalone-handler-runtime.js.map +1 -0
  155. package/dist/server/environment-attachment.d.ts.map +1 -1
  156. package/dist/server/environment-attachment.js +2 -1
  157. package/dist/server/environment-attachment.js.map +1 -1
  158. package/dist/server/environment-delivery-worker.d.ts.map +1 -1
  159. package/dist/server/environment-delivery-worker.js +21 -12
  160. package/dist/server/environment-delivery-worker.js.map +1 -1
  161. package/dist/server/managed-server-application.d.ts +4 -1
  162. package/dist/server/managed-server-application.d.ts.map +1 -1
  163. package/dist/server/managed-server-application.js +2 -2
  164. package/dist/server/managed-server-application.js.map +1 -1
  165. package/dist/server/node-coordinator.js +2 -2
  166. package/dist/server/node-coordinator.js.map +1 -1
  167. package/dist/server/server-log.d.ts.map +1 -1
  168. package/dist/server/server-log.js +4 -0
  169. package/dist/server/server-log.js.map +1 -1
  170. package/dist/server/server.d.ts +9 -177
  171. package/dist/server/server.d.ts.map +1 -1
  172. package/dist/server/server.js +428 -178
  173. package/dist/server/server.js.map +1 -1
  174. package/dist/services/query-reader.d.ts +19 -0
  175. package/dist/services/query-reader.d.ts.map +1 -0
  176. package/dist/services/query-reader.js +43 -0
  177. package/dist/services/query-reader.js.map +1 -0
  178. package/dist/services/registered-targets.d.ts +73 -0
  179. package/dist/services/registered-targets.d.ts.map +1 -0
  180. package/dist/services/registered-targets.js +127 -0
  181. package/dist/services/registered-targets.js.map +1 -0
  182. package/dist/services/spine-services.d.ts +26 -12
  183. package/dist/services/spine-services.d.ts.map +1 -1
  184. package/dist/services/spine-services.js +1047 -969
  185. package/dist/services/spine-services.js.map +1 -1
  186. package/dist/spi/delivery.d.ts +5 -0
  187. package/dist/spi/delivery.d.ts.map +1 -0
  188. package/dist/spi/delivery.js +18 -0
  189. package/dist/spi/delivery.js.map +1 -0
  190. package/dist/spi/handler-registry.d.ts +8 -0
  191. package/dist/spi/handler-registry.d.ts.map +1 -0
  192. package/dist/spi/handler-registry.js +15 -0
  193. package/dist/spi/handler-registry.js.map +1 -0
  194. package/dist/stand/stand.d.ts +73 -1
  195. package/dist/stand/stand.d.ts.map +1 -1
  196. package/dist/stand/stand.js +221 -48
  197. package/dist/stand/stand.js.map +1 -1
  198. package/dist/stand/subscription-observer.d.ts.map +1 -1
  199. package/dist/stand/subscription-observer.js +5 -12
  200. package/dist/stand/subscription-observer.js.map +1 -1
  201. package/dist/stand/subscription-registry.js +1 -1
  202. package/dist/stand/subscription-registry.js.map +1 -1
  203. package/dist/testing/index.d.ts +25 -3
  204. package/dist/testing/index.d.ts.map +1 -1
  205. package/dist/testing/index.js +23 -3
  206. package/dist/testing/index.js.map +1 -1
  207. package/dist/tsconfig.tsbuildinfo +1 -1
  208. package/package.json +33 -23
  209. package/dist/handler/build-time-handler-analyzer.d.ts +0 -150
  210. package/dist/handler/build-time-handler-analyzer.d.ts.map +0 -1
  211. package/dist/handler/build-time-handler-analyzer.js +0 -1101
  212. package/dist/handler/build-time-handler-analyzer.js.map +0 -1
  213. package/dist/handler/generated-registry-writer.d.ts +0 -58
  214. package/dist/handler/generated-registry-writer.d.ts.map +0 -1
  215. package/dist/handler/generated-registry-writer.js +0 -458
  216. package/dist/handler/generated-registry-writer.js.map +0 -1
  217. package/dist/handler/handler-codegen.d.ts +0 -48
  218. package/dist/handler/handler-codegen.d.ts.map +0 -1
  219. package/dist/handler/handler-codegen.js +0 -214
  220. package/dist/handler/handler-codegen.js.map +0 -1
  221. package/dist/server/browser-server.d.ts +0 -49
  222. package/dist/server/browser-server.d.ts.map +0 -1
  223. package/dist/server/browser-server.js.map +0 -1
  224. package/dist/server/durable-subscription-bindings.d.ts.map +0 -1
  225. package/dist/server/durable-subscription-bindings.js.map +0 -1
  226. /package/dist/{server → browser}/durable-subscription-bindings.d.ts +0 -0
  227. /package/dist/{server → browser}/durable-subscription-bindings.js +0 -0
package/REFERENCE.md CHANGED
@@ -1,6 +1,81 @@
1
1
  # @spine-event-engine/server reference
2
2
 
3
- This reference describes the public server contracts for coding agents.
3
+ This reference gives application developers and coding agents the detailed
4
+ server contracts. For an introduction and examples, start with the
5
+ [server README](README.md).
6
+
7
+ ## Testing entry point
8
+
9
+ `@spine-event-engine/server/testing` is for framework-package tests and controlled
10
+ in-process fixtures. It exposes test reset and integration-frame helpers; managed-host
11
+ internals remain repository-only fixture code. This entrypoint is deliberately outside
12
+ the end-user root API and may change with test infrastructure rather than normal server
13
+ compatibility guarantees.
14
+
15
+ ## Delivery SPI
16
+
17
+ Delivery adapters import `conditionalPickUp` from
18
+ `@spine-event-engine/server/spi/delivery`. It registers or looks up an optional
19
+ conditional-pickup capability for a `DeliveryWorkRegistry`; it is not part of
20
+ the server root API and does not independently create delivery work.
21
+
22
+ ## Generated handler-registry SPI
23
+
24
+ Generated model modules import only their registry data types from
25
+ `@spine-event-engine/server/spi/handler-registry`. The stable contract is the
26
+ unversioned `GeneratedHandlerRegistry` receiver collection, receiver records,
27
+ handler records, kinds, arity, and optional `where` data. The subpath has no
28
+ runtime values. Server-side ingestion, validation errors, and their error codes
29
+ are implementation details; generated code must not depend on them. Retired
30
+ versioned registries are rejected and must be regenerated.
31
+
32
+ ## Standalone handlers
33
+
34
+ Generated standalone handlers extend `AbstractAssignee`, `AbstractCommander`,
35
+ `AbstractEventReactor`, or `AbstractEventSubscriber`. Register instances with
36
+ `addAssignee()`, `addCommandDispatcher()`, or `addEventDispatcher()` as their
37
+ role requires, then use `buildAsync()` to load generated receiver metadata and
38
+ match the exact constructor. Assignees consume Commands and produce Events;
39
+ commanders consume Commands, Events, or rejections and produce Commands;
40
+ reactors consume Events or rejections and may produce Events; subscribers
41
+ consume Events, rejections, or Entity states and produce no signal. State
42
+ subscribers use the System Event Bus. Produced signals are published in-process
43
+ on a best-effort basis; this is not an exactly-once contract.
44
+
45
+ ## Entity constructor dependencies
46
+
47
+ Supply `onCreate` when an Entity constructor needs an application service:
48
+ `new Repository({ entityType, schema, onCreate })` for an explicit repository,
49
+ or `.add(EntityClass, { onCreate })` when Spine creates the repository. The
50
+ callback receives `EntityOptions` with the registered class's exact identifier
51
+ and state-schema types. Its result must be an instance of that class.
52
+
53
+ `onCreate` is required when the class cannot be constructed with framework
54
+ options alone. It is optional for constructors that need only `EntityOptions`, or
55
+ constructors whose additional parameters are optional or have defaults.
56
+
57
+ For each new object, the callback must synchronously return a fresh instance
58
+ and pass the supplied options unchanged to its constructor. Those options
59
+ contain the ID, schema, state, Spine `Version`, and lifecycle flags. The same
60
+ callback runs when restoring a stored Aggregate, Projection, or Process Manager,
61
+ and when creating a Projection during a rebuild. It is not a once-per-ID hook.
62
+ Queries that read stored state without constructing an Entity do not call it.
63
+
64
+ Construct and initialize shared services before building the context. Keep
65
+ network calls and other business work in handlers, not in `onCreate`. A thrown
66
+ construction error fails that dispatch before its handler runs; it is not a
67
+ domain rejection. An asynchronous callback or a result of the wrong class is
68
+ rejected. Services are neither serialized nor closed by Spine; close them in
69
+ application shutdown after closing the context.
70
+
71
+ Spine still connects Entity history and Process Manager queries. A constructor
72
+ callback does not receive the current actor or tenant. Pass these to a service
73
+ from the handler when needed. Queries made through the Process Manager still
74
+ use the handler's context. Services are passed directly; no dependency container
75
+ is required, and the storage format is unchanged.
76
+
77
+ See the [Entity dependency guide](https://github.com/SpineEventEngine/spine-ts/blob/master/docs/USER_GUIDE.md#give-an-entity-an-application-service)
78
+ and the [Projects example](https://github.com/SpineEventEngine/spine-ts/blob/master/examples/projects/README.md).
4
79
 
5
80
  ## Integration broker and event origin
6
81
 
@@ -8,8 +83,8 @@ Each `BoundedContext` creates and closes exactly one private integration broker.
8
83
  The broker has distinct status, configuration, and event exchanges. Event
9
84
  export is requested-only: a context installs a domestic publisher only for
10
85
  event types requested by another context, and withdrawal removes only that
11
- requester's interest. The broker does not enforce ownership; the supported
12
- shape is many consumers and one domain producer per event type at a time.
86
+ requester's interest. Each event type should have one producing domain and may
87
+ have many consumers; the broker does not enforce that producer restriction.
13
88
 
14
89
  The status exchange announces `BoundedContextOnline`; observing a foreign
15
90
  announcement resends the local complete wanted-event document. The
@@ -95,13 +170,64 @@ To override the default channel factory, add
95
170
 
96
171
  ## Routing lifecycle
97
172
 
98
- Event, command, and state-update routing accepts exact schemas and nominal
99
- message-interface tokens. It selects exact, first registered matching token,
100
- then replacement/default. The route runs once at accepted admission; its
101
- validated typed targets are stored and reused for retries. The legacy-named
173
+ Event, command, and state-update routing accepts exact schemas and generated
174
+ message-interface tokens. It selects an exact schema first, then the first
175
+ matching token, then the replacement or default route. The route runs when the message is accepted; its
176
+ validated typed targets are stored and reused for retries. Route callbacks may
177
+ return a value or a Promise. `CommandRouting.create(EntityClass)`,
178
+ `EventRouting.create(EntityClass)`, and `StateUpdateRouting.create(ProjectionClass)`
179
+ infer the receiving ID, state, and application Entity types. The selected class
180
+ must be the same constructor registered by the repository;
181
+ construction rejects a mismatch even when two classes share ID and state types.
182
+ The callback's third argument exposes `findIds(query)`, `findStates(query)`, and `find(query)`
183
+ for that repository in the signal's tenant. The read access rejects new calls
184
+ after the route finishes. ID-only `create<Id>()` declarations remain valid.
185
+ Direct `repository.routeCommand()` and `repository.routeEvent()` calls return
186
+ Promises and must be awaited. Commands select exactly one ID; Events and state
187
+ updates may select any number of distinct IDs. After validation and stable
188
+ deduplication, routes above 1,000 recipients emit one warning and retain every
189
+ recipient. Inbox replay uses stored target IDs without rerouting. The legacy-named
102
190
  local `catchUpReadSide()` helper is unrelated to retry: it resets and replays
103
191
  the whole local read side, and is not Projection catch-up.
104
192
 
193
+ ## Message-valued Entity IDs
194
+
195
+ The first state field determines the Entity ID type. It can be a primitive or
196
+ a generated Protobuf message; for a message ID, all declared fields form the
197
+ identifier. Nested and composite IDs are supported.
198
+
199
+ ```proto
200
+ message ShipmentId {
201
+ spine.core.UserId owner = 1;
202
+ int32 number = 2;
203
+ }
204
+
205
+ message Shipment {
206
+ ShipmentId id = 1;
207
+ string status = 2;
208
+ }
209
+ ```
210
+
211
+ The root `MessageId` type accepts generated messages such as `CommandId`:
212
+
213
+ ```ts
214
+ import { create } from "@bufbuild/protobuf";
215
+ import { type MessageId } from "@spine-event-engine/server";
216
+ import { CommandIdSchema } from "@spine-event-engine/proto";
217
+
218
+ const id: MessageId = create(CommandIdSchema, { uuid: "shipment-7" });
219
+ ```
220
+
221
+ Admission validates a message ID against the schema declared by the state
222
+ field, including generated validation rules, before it is stored or replayed.
223
+ When default routing finds a message but the Entity ID type is primitive, a
224
+ custom route returns the intended primitive ID.
225
+
226
+ Without a custom route, Command routing uses the Command's declared first
227
+ field. Event routing uses a compatible producer ID when available and otherwise
228
+ uses the Event's declared first field. State-update routing selects the first
229
+ state field compatible with the Entity ID.
230
+
105
231
  ## Handler routing and operations
106
232
 
107
233
  Generated handler metadata routes a command, event, or state update by its
@@ -110,6 +236,291 @@ there is no exact route. This is TypeScript routing; it does not consume
110
236
  `(is).java_type` or `(every_is).java_type`, and it has no decorator-based
111
237
  route registration API.
112
238
 
239
+ Handlers may return their established result directly or through exactly one built-in `Promise<T>`.
240
+ Nested promises and structural or imported thenable lookalikes are rejected during handler analysis.
241
+ The entity transaction remains open until that promise settles; rejection rolls
242
+ back framework state and suppresses produced output, but cannot undo external
243
+ side effects. Process Managers may use their protected read-only query surface
244
+ for eventually consistent Entity state during a handler. Aggregates must not
245
+ use these reads for invariants.
246
+
247
+ `select(schema, columns)` supports `byId`, typed `where`, `orderBy`,
248
+ `limit`, `read`, `findById`, and `all`. A Process Manager query returns at most
249
+ 1,000 states and accepts at most 1,000 explicit IDs at execution. A generated
250
+ `StateQuery.create()...build()` value also works with `this.select(query).read()`; its read
251
+ binds the current handler actor and tenant. `all()` is a convenience and can be costly on a large read model;
252
+ prefer an ID-targeted or ordered bounded query.
253
+
254
+ Use `EntityQuery.all(...)` to require every predicate and `EntityQuery.either(...)`
255
+ to accept any branch. Query execution inherits the active handler's actor and
256
+ effective tenant; the protected read-only facade has no tenant override.
257
+
258
+ `RepositoryReadQueries<EntityType>` describes the receiving repository's typed
259
+ `findIds`, `findStates`, and `find` reads. A read returns all matches unless the
260
+ shared generated query sets a limit. States are detached messages; `find`
261
+ restores application Entity instances with stored Version and lifecycle flags.
262
+ The framework binds each read scope to the incoming signal tenant. Routing
263
+ callbacks receive that scope as their third argument while they run.
264
+
265
+ For contexts registered with one `Server`, the target Entity's type URL selects
266
+ its context. No context name or network request is required. The Server rejects
267
+ duplicate Entity type registrations before starting delivery recovery or
268
+ listening, identifying the type and both contexts. Contexts used without a
269
+ Server retain local querying; separate Servers do not share query routes.
270
+
271
+ Cross-context targets must declare `query` or `full` visibility. This applies
272
+ to any Entity family with queryable state. Unknown targets, foreign targets
273
+ without query visibility, and tenant mismatches produce errors before storage
274
+ reads. Existing local and public-query visibility behavior is unchanged.
275
+
276
+ Single-tenant execution resolves to the built-in `SINGLE_TENANT` identity;
277
+ omitting the wire tenant field does not create tenant-free execution. Named
278
+ tenants can query their corresponding multitenant partition, not a single-tenant
279
+ destination. `SINGLE_TENANT` can query a single-tenant destination or its same
280
+ partition in a multitenant destination. The query never replaces the tenant to
281
+ suit the target. A valid query with no matching record keeps its ordinary empty
282
+ result (`undefined` from `findById()`). Reads remain eventually consistent.
283
+
284
+ Routes are available before recovered handlers execute. Server shutdown drains
285
+ accepted handlers across contexts before closing any target read-side; failed
286
+ startup validation participates in context and resource cleanup.
287
+
288
+ ```ts
289
+ import type { Message } from "@bufbuild/protobuf";
290
+ import type { GenMessage } from "@bufbuild/protobuf/codegenv2";
291
+ import { EntityQuery, type EntityColumn } from "@spine-event-engine/core";
292
+ import { ProcessManager } from "@spine-event-engine/server";
293
+
294
+ type RequestView = Message<"RequestView"> & {
295
+ id: string;
296
+ status: string;
297
+ createdAt: number;
298
+ };
299
+
300
+ declare const RequestViewSchema: GenMessage<RequestView>;
301
+ declare const RequestViewColumns: {
302
+ readonly status: EntityColumn<typeof RequestViewSchema, "status", string, "equal">;
303
+ readonly createdAt: EntityColumn<
304
+ typeof RequestViewSchema,
305
+ "createdAt",
306
+ number,
307
+ "equal" | "greaterThan"
308
+ >;
309
+ };
310
+
311
+ abstract class RequestCoordinator extends ProcessManager<string, typeof RequestViewSchema> {
312
+ protected async pendingRequests(requestId: string): Promise<void> {
313
+ const matching = await this.select(RequestViewSchema, RequestViewColumns)
314
+ .where(EntityQuery.eq(RequestViewColumns.status, "pending"))
315
+ .orderBy(RequestViewColumns.createdAt, "asc")
316
+ .limit(10)
317
+ .read();
318
+ const one = await this.select(RequestViewSchema, RequestViewColumns).findById(requestId);
319
+ const everyVisible = await this.select(RequestViewSchema, RequestViewColumns).all();
320
+ void [matching, one, everyVisible];
321
+ }
322
+ }
323
+ ```
324
+
325
+ `all()` remains capped at 1,000 states but may still be costly; use `findById()`
326
+ or an ordered bounded `read()` where possible.
327
+
328
+ A command-input `@Command` method is a command substitution receptor: it is
329
+ the one effective receptor for that Command type (instead of an `@Assign`),
330
+ commits its Entity state before its one-or-more returned Commands are detached
331
+ for in-process produced-command enqueue, and receives an optional `CommandContext`.
332
+ Only Process Manager repositories support `@Command` handlers. Aggregate and
333
+ Projection repositories reject command-input substitutions and event- or
334
+ rejection-input command reactions during generated metadata ingestion and
335
+ repository construction.
336
+ Event- and rejection-input `@Command` methods remain Event- or
337
+ rejection-to-command reactions on Event Bus.
338
+
339
+ ### Handler return types
340
+
341
+ Use a TypeScript union such as `CreateAccessGrant | ExtendAccessGrant` to return
342
+ one of several Commands. A tuple such as
343
+ `readonly [AccessGrantCreated, AccessRequestCompleted?]` returns multiple Events
344
+ in order, with the second optional. Either declaration may have one outer
345
+ `Promise` layer. Generated metadata lists every possible schema; runtime checks
346
+ each result against the invoked handler's list. A whole result may be absent
347
+ for `@React` and Event/rejection-input `@Command`, using a generated signal type
348
+ unioned with `undefined`. A reaction that never emits can declare `undefined`
349
+ alone or `Promise<undefined>`. Either reaction kind may also return an empty
350
+ typed array. Command-input `@Command` and `@Assign` require nonempty results.
351
+ `@Subscribe` declares `void` or `Promise<void>` and produces no signals.
352
+ `@Throws` declares thrown rejections separately from normal results.
353
+
354
+ Only subscriptions accept `void`. An absent reaction result or optional tuple
355
+ entry uses `undefined`, never `null`. The framework validates all returned
356
+ values before committing Entity changes or publishing outputs. An invalid
357
+ subscription result fails even if it is an empty array. A valid no-output
358
+ reaction still saves its Entity state changes.
359
+
360
+ Other supported forms are `T[]`, `readonly T[]`, `Array<T>`, `ReadonlyArray<T>`,
361
+ named tuple entries, union alternatives inside tuple entries, and local or
362
+ imported aliases resolving to concrete generated messages. Only one flat
363
+ collection level is supported. Rest tuples, nested collections, nested promises,
364
+ custom thenables, framework envelopes, and unresolved/`any`/`unknown` result
365
+ branches are rejected. TypeScript enforces tuple structure; the runtime checks
366
+ declared message types, required nonempty results, and result order, not tuple
367
+ arity or positional schema constraints. See the
368
+ [user guide](https://github.com/SpineEventEngine/spine-ts/blob/master/docs/USER_GUIDE.md#choose-what-a-handler-returns) for a compact
369
+ return-type table and examples.
370
+
371
+ These independent examples use generated review-workflow messages. The Commander
372
+ chooses whether to start or schedule a review. The reactor example shows a
373
+ different workflow: record a started review and optionally schedule a follow-up.
374
+ They illustrate return declarations, not two handlers to register together.
375
+
376
+ <!-- docs-snippet-path: packages/server/test/runtime/standalone-handler-runtime.test.ts -->
377
+
378
+ ```ts
379
+ import { create } from "@bufbuild/protobuf";
380
+ import {
381
+ AbstractCommander,
382
+ AbstractEventReactor,
383
+ Command,
384
+ React,
385
+ } from "@spine-event-engine/server";
386
+ import {
387
+ StartReviewSchema,
388
+ ScheduleReviewSchema,
389
+ type StartReview,
390
+ type ScheduleReview,
391
+ } from "../../test-fixtures/generated/handler-registry/commands_pb.js";
392
+ import {
393
+ ReviewStartedSchema,
394
+ ReviewFollowUpScheduledSchema,
395
+ type ReviewStarted,
396
+ type ReviewFollowUpScheduled,
397
+ type ReviewTaskAssigned,
398
+ } from "../../test-fixtures/generated/handler-registry/events_pb.js";
399
+
400
+ export class ReviewCommander extends AbstractCommander {
401
+ @Command
402
+ planReview(event: ReviewTaskAssigned): StartReview | ScheduleReview {
403
+ // Return one Command, choosing its type using the application's rules.
404
+ return event.name === "Security review"
405
+ ? create(ScheduleReviewSchema, { id: event.id })
406
+ : create(StartReviewSchema, { id: event.id });
407
+ }
408
+ }
409
+
410
+ export class ReviewReactor extends AbstractEventReactor {
411
+ @React
412
+ recordReview(event: ReviewTaskAssigned): readonly [ReviewStarted, ReviewFollowUpScheduled?] {
413
+ const started = create(ReviewStartedSchema, { id: event.id });
414
+ // A tuple returns ordered Events. Its optional second value may be absent.
415
+ if (event.name !== "Security review") return [started];
416
+ return [
417
+ started,
418
+ create(ReviewFollowUpScheduledSchema, { id: event.id, name: "Security follow-up" }),
419
+ ];
420
+ }
421
+ }
422
+ ```
423
+
424
+ Produced commands retain the source actor, tenant, origin, and causal lineage.
425
+ The enqueue is post-commit best effort, not an atomic outbox or exactly-once
426
+ delivery: a process crash between commit and enqueue can lose a child. Accepted
427
+ produced commands drain during context close. A contained child failure is diagnosed,
428
+ but does not retroactively fail an already accepted source command or durably
429
+ retry that child.
430
+
431
+ For a generated Process Manager, a command-input handler uses distinct domain
432
+ Command input and output types, and the generated registry supplies those
433
+ schemas. This self-contained example explicitly ingests the unversioned data that an
434
+ application build normally emits, then assembles a repository from it; it does
435
+ not claim that its fixture discovers a registry artifact automatically:
436
+
437
+ <!-- docs-snippet-path: packages/server-blackbox-tests/test/project-event-routing.test.ts -->
438
+
439
+ ```ts
440
+ import { create } from "@bufbuild/protobuf";
441
+ import { SignalEnvelopes } from "@spine-event-engine/core";
442
+ import {
443
+ CommandContextSchema,
444
+ CommandIdSchema,
445
+ type CommandContext,
446
+ } from "@spine-event-engine/proto";
447
+ import {
448
+ BoundedContext,
449
+ Command,
450
+ type EntityHandlersMetadata,
451
+ HandlerRegistryIngestor,
452
+ ProcessManager,
453
+ Repository,
454
+ } from "@spine-event-engine/server";
455
+ import type { GeneratedHandlerRegistry } from "@spine-event-engine/server/spi/handler-registry";
456
+
457
+ import {
458
+ ApproveProjectSchema,
459
+ ScheduleProjectSchema,
460
+ type ApproveProject,
461
+ type ScheduleProject,
462
+ } from "../generated/spine/server/testing/project_commands_pb.js";
463
+ import {
464
+ CoordinationStateSchema,
465
+ OrganizationIdSchema,
466
+ ProjectIdSchema,
467
+ type ProjectId,
468
+ } from "../generated/spine/server/testing/project_workflow_pb.js";
469
+
470
+ class ApprovalCoordinator extends ProcessManager<ProjectId, typeof CoordinationStateSchema> {
471
+ @Command
472
+ approve(command: ApproveProject, context: CommandContext): ScheduleProject {
473
+ this.update((draft) => Object.assign(draft, { id: this.id, projectName: command.status }));
474
+ return create(ScheduleProjectSchema, { project: command.project, status: command.status });
475
+ }
476
+ }
477
+
478
+ const project = create(ProjectIdSchema, {
479
+ organization: create(OrganizationIdSchema, { code: "org-a" }),
480
+ number: 1,
481
+ });
482
+ const registry: GeneratedHandlerRegistry = {
483
+ receivers: [
484
+ {
485
+ receiverKind: "entity",
486
+ receiverType: ApprovalCoordinator,
487
+ stateSchema: CoordinationStateSchema,
488
+ handlers: [
489
+ {
490
+ kind: "command-substitution",
491
+ methodName: "approve",
492
+ input: { schema: ApproveProjectSchema, origin: "domestic" },
493
+ outcomes: { returned: [ScheduleProjectSchema], thrown: [] },
494
+ parameterCount: 2,
495
+ },
496
+ ],
497
+ },
498
+ ],
499
+ };
500
+ const [handlers] = new HandlerRegistryIngestor().ingest(registry);
501
+ if (handlers === undefined) throw new Error("Generated Process Manager metadata is missing.");
502
+ const repository = new Repository<typeof ApprovalCoordinator>({
503
+ entityType: ApprovalCoordinator,
504
+ schema: CoordinationStateSchema,
505
+ handlers: handlers as EntityHandlersMetadata<ApprovalCoordinator, typeof CoordinationStateSchema>,
506
+ });
507
+ const context = BoundedContext.singleTenant("Projects").add(repository).build();
508
+ await context.commandBus().post(
509
+ SignalEnvelopes.command({
510
+ context: create(CommandContextSchema),
511
+ schema: ApproveProjectSchema,
512
+ message: create(ApproveProjectSchema, { project, status: "approved" }),
513
+ }),
514
+ );
515
+ ```
516
+
517
+ The generated record declares `ApproveProjectSchema` as the input and
518
+ `ScheduleProjectSchema` as the emitted Command schema; registry ingestion, not
519
+ manual decorator materialization, constructs the output-bearing metadata.
520
+ Application builds emit the record to
521
+ `generated/handler/generated-handler-registry.js`; those applications use
522
+ `buildAsync()` with their compiled package root.
523
+
113
524
  One `@Where({ eventField, equals })` equality filter may narrow an event- or
114
525
  rejection-consuming `@Subscribe`, `@React`, or `@Command` handler after type
115
526
  routing. `eventField` and `equals` are typed string literals; invalid or
@@ -132,6 +543,10 @@ handler registry for classes registered with `add(EntityClass)`;
132
543
  explicit `Repository` registration. A built context contains `CommandBus`,
133
544
  `EventBus`, `Stand`, repositories, and its storage lifecycle.
134
545
 
546
+ Generated writer output is an unversioned receiver collection and records
547
+ command substitutions explicitly. The ingestor rejects retired versioned
548
+ registry shapes with an instruction to regenerate.
549
+
135
550
  ### Stand subscription registry
136
551
 
137
552
  Each built context also has one `StandSubscriptionRegistry`. By default the
@@ -144,8 +559,8 @@ the builder on the first build attempt.
144
559
 
145
560
  `SubscriptionService` streams are live best-effort notifications, not replay,
146
561
  ordering, or completeness contracts. A standalone Server defaults to the
147
- persistent registry. In a managed complete-replica cohort, the framework owns
148
- each child Server and accepts only every context's actual
562
+ persistent registry. In a group of managed server replicas, the framework starts
563
+ and closes each child Server and accepts only every context's actual
149
564
  `InMemorySubscriptionRegistry`; a persistent or custom registry is rejected
150
565
  and the assembled child is closed before READY. Durable logical bindings remain
151
566
  Gateway-only. The Coordinator fans each logical definition to ready children,
@@ -156,8 +571,8 @@ child streams before admission; subscribed but inactive definitions do not
156
571
  block readiness. Each child observes application-configured remote/shared
157
572
  Delivery directly. During DRAINING, new unary and shard admission stops while
158
573
  active Delivery and its subscription updates finish before child close. The
159
- application owns Delivery facility and shard-strategy selection; managed mode
160
- does not infer or certify configuration provenance.
574
+ application configures the Delivery facility and selects the shard strategy;
575
+ managed mode does not infer or certify configuration provenance.
161
576
 
162
577
  Create records begin `pending` and expire after 30 seconds unless activated.
163
578
  Active records have no framework TTL. Cancellation physically deletes the
@@ -202,11 +617,21 @@ record-storage handle, while the context closes the registry.
202
617
 
203
618
  `Entity` is the state base class. `Aggregate`, `Projection`, and
204
619
  `ProcessManager` identify the three entity families. Handler decorators are
205
- `@Assign`, `@Command`, `@React`, `@Subscribe`, and `@Apply`. In a transactional
620
+ `@Assign`, `@Command`, `@React`, and `@Subscribe`. A command-accepting
621
+ handler uses `@Throws(GeneratedRejection)` to declare its possible domain
622
+ rejections. Write the primary handler decorator first and `@Throws` immediately
623
+ below it; declaration order does not affect behavior. Generated metadata records
624
+ normal and rejection outcomes before context assembly, so clients may subscribe
625
+ to a declared rejection even when no server handler consumes it. Throwing an
626
+ undeclared generated rejection is a technical handler failure. In a transactional
206
627
  handler, `update(mutator)` changes the active draft and returns the draft;
207
628
  `tryUpdate(mutator)` validates a scratch draft and returns violations without
208
629
  applying an invalid change. Entity lifecycle and version changes are committed
209
630
  only after the transaction accepts.
631
+ Every Entity exposes the generated Spine `Version` message, starting at number
632
+ zero. A dispatch that produces Events or changes state or lifecycle advances it
633
+ once; no-op and rejected dispatches do not. Repositories restore and persist the
634
+ full Version, including its timestamp, across current state, history, and Stand.
210
635
 
211
636
  ## Signals, validation, and rejection behavior
212
637
 
@@ -216,10 +641,13 @@ command-validation failure, which `SpineServices` maps to
216
641
  `COMMAND_VALIDATION_ERROR` with a packed `spine.validation.ValidationError`.
217
642
  Entity transition validation failures map to
218
643
  `COMMAND_STATE_TRANSITION_VALIDATION_FAILED` with the same detail type.
644
+ Framework-created runtime command and event metadata uses fresh Node secure
645
+ UUIDs; fixed IDs belong only to existing source envelopes retained through the
646
+ normal origin chain.
219
647
 
220
648
  An application handler throws a generated core `RejectionThrowable` for a
221
- domain rejection. The repository rolls back state, version, lifecycle, and
222
- output; it schedules the typed rejection event independently. Command service
649
+ domain rejection declared with `@Throws`. The repository rolls back state,
650
+ version, lifecycle, and output; it schedules the typed rejection event independently. Command service
223
651
  acknowledgement remains an accepted `Ack`. Rejection-event posting can be
224
652
  unobserved by inactive, full, or closed subscriptions and a posting failure is
225
653
  an internal diagnostic, not a retry guarantee.
@@ -261,7 +689,7 @@ callback failures are isolated from post-commit observation and other
261
689
  consumers. The package does not guarantee cluster-complete observations,
262
690
  subscription replay, event-gap repair, or exactly-once effects.
263
691
 
264
- ### Standalone and browser hosting
692
+ ### Native and managed hosting
265
693
 
266
694
  `Server.start()` installs no process signal handlers, never closes its
267
695
  environment, and shares only a caller-managed active generation. `Server.run()`
@@ -273,15 +701,15 @@ signal or explicit-close retry, and sets `process.exitCode` to `1` after a
273
701
  signal-driven failure.
274
702
 
275
703
  `ManagedServerApplication.run()` is the Node-only deployment entrypoint for a
276
- complete-replica process cohort when the framework owns process shutdown.
277
- `ManagedServerApplication.start()` starts the same cohort for an embedding host
704
+ group of complete server replicas when the framework handles process shutdown.
705
+ `ManagedServerApplication.start()` starts the same group for an embedding host
278
706
  and installs no process signal handlers; that host closes the returned handle.
279
707
  Both require a positive safe-integer
280
708
  `processCount` and an ESM `moduleUrl`; it never derives a process count from
281
709
  the machine. Its optional `host` and required nonzero `port` bind the public
282
- Coordinator listener. Every child executes that same module and calls its own
283
- `createServer({ host: "127.0.0.1", port: 0 })`. The child reports its actual
284
- loopback endpoint only after its local readiness gates settle. The parent
710
+ Coordinator listener. Every child executes that same module and calls the
711
+ module's `createServer({ host: "127.0.0.1", port: 0 })`. The child reports its
712
+ actual loopback endpoint only after its local readiness gates settle. The parent
285
713
  initially waits until every configured child is ready. Afterwards it is ready
286
714
  while at least one child is ready; it remains alive and keeps replacing while
287
715
  degraded, and reports unready if no child is ready. It replaces an unexpected
@@ -303,12 +731,10 @@ call on a sibling child. Subscription fan-out follows current READY members;
303
731
  later and replacement members attach the retained subscription before managed
304
732
  admission.
305
733
 
306
- `ServerOptions.browser` changes the public listener, not the bounded-context
307
- services. The native HTTP/2 backend binds to an ephemeral loopback port and is
308
- never returned. After it is ready, the server creates `UnaryGateway`,
309
- `SubscriptionGateway`, and native gateway services, then binds one HTTP/1.1
310
- Connect/gRPC-Web listener. A public bind failure closes subscription resources
311
- and the native backend; multiple rollback failures are aggregated.
734
+ `ServerOptions` is native-only: it configures contexts, resources, services,
735
+ and the native listener. It has no `browser` option and the root package does
736
+ not load `@spine-event-engine/auth`. Browser hosting belongs to the explicit
737
+ `@spine-event-engine/server/browser` entry point; see the [browser/auth guide](https://github.com/SpineEventEngine/spine-ts/blob/master/docs/BROWSER_CLIENT_AUTH_EXTENSION_GUIDE.md).
312
738
 
313
739
  Browser unary gateway admission has a fixed 1 MiB (1,048,576-byte) request
314
740
  limit. A larger unary gateway request is rejected with `ResourceExhausted`.
@@ -317,7 +743,7 @@ gRPC-Web transport message accepted by the public listener, so the effective
317
743
  inbound limit is the stricter applicable limit. `writeMaxBytes` limits public
318
744
  transport responses independently.
319
745
 
320
- Browser origins must be unique canonical HTTP(S) origins with no path, query,
746
+ For `BrowserServer` from `@spine-event-engine/server/browser`, origins must be unique canonical HTTP(S) origins with no path, query,
321
747
  fragment, or trailing slash. Requests without an allowed exact `Origin` receive
322
748
  403 before RPC handling. Allowed responses include credentialed exact-origin
323
749
  CORS, `Vary: Origin`, protocol request headers, and exposed gRPC status headers.
@@ -364,12 +790,12 @@ native backend. Concurrent calls share an attempt; a failed unfinished phase
364
790
  can be retried without repeating completed native cleanup.
365
791
 
366
792
  Browser subscription bindings are separate from service subscription records.
367
- `BrowserAdmission` selects their ownership. Authenticated mode supplies
793
+ `BrowserAdmission` determines who configures and manages them. Authenticated mode supplies
368
794
  `sessions` and may supply `BrowserServerOptions.bindings`; production then
369
- requires the Server package's `DurableSubscriptionBindings` and rejects a
795
+ requires `DurableSubscriptionBindings` from `@spine-event-engine/server/browser` and rejects a
370
796
  missing or in-memory binding store before listener open. Public mode supplies
371
- `publicAccess: true`, cannot supply bindings, and uses framework-owned
372
- process-local bindings. The durable registry receives an explicit application
797
+ `publicAccess: true`, cannot supply bindings, and uses process-local bindings
798
+ managed by the framework. The durable registry receives an explicit application
373
799
  namespace, storage factory, identifier source, and cleanup callback. It closes
374
800
  only its independently opened record-storage handle, not the application
375
801
  storage factory or a Spine JVM/TS backend. It stores canonical public
@@ -383,7 +809,7 @@ inside the stored Topic and are checked for Activate and Cancel. Create assigns
383
809
  the public ID directly; no quota, reservation, fingerprint, or lease is
384
810
  persisted. Cleanup remains bounded and restart-safe at the record level. It
385
811
  cleans the backend definition before deleting its record. It is not a cleaner
386
- lease or fence: this direct store supports one Gateway process. Active streams do not resume,
812
+ lock between Gateway processes: this store supports one Gateway process. Active streams do not resume,
387
813
  updates are not replayed, and the registry provides neither exactly-once
388
814
  delivery, global update ordering, nor cluster-complete notification delivery.
389
815
 
@@ -402,18 +828,23 @@ rows are direct records; a delivered row is the deduplication fact. A handler
402
828
  effect and the exact pending-to-delivered compare-and-set are not one
403
829
  transaction, so a lost acknowledgement can redeliver after restart. Downstream
404
830
  signal handling must be idempotent. A customized `DeliveryWorkRegistry`
405
- implements `validateOwnership()` so delivery can validate before dispatch, at
406
- repository commit time, and before acknowledgement. Validation is a fence
407
- against an observed takeover, not a distributed transaction with handler
408
- storage.
831
+ implements `validateOwnership()` so delivery can validate before dispatch, before
832
+ the Entity transaction commit, and before acknowledgement. These checks stop a worker
833
+ after it detects that another worker has taken the shard. They do not make
834
+ delivery and handler storage part of one distributed transaction.
409
835
 
410
836
  `keepUntil` is the optional deduplication-protection deadline, not a second
411
837
  retention setting. A delivered row is cleanup-eligible when that deadline is
412
- absent or elapsed. Under current shard ownership, environment delivery performs
413
- one bounded cleanup page, plus at most one continuation after a full protected
414
- page makes no removal. Each removal atomically verifies ownership and the exact
415
- delivered snapshot. Pending, retryable, non-delivered, and still-protected rows
416
- remain. There is no additional retention configuration, timer, or scheduler.
838
+ absent or elapsed (`keepUntil <= now`). Delivery skips per-record mutation checks
839
+ and deletion attempts for rows whose deadline is still in the future. Cleanup
840
+ and retained-row duplicate recognition use the same applicable clock. Eligible
841
+ rows still receive final time and shard checks before deletion.
842
+ Direct local storage atomically verifies ownership and
843
+ the exact delivered snapshot during removal. A remote adapter may instead read
844
+ and compare the snapshot before sending a separate best-effort removal request;
845
+ that sequence is not an atomic compare-and-delete guarantee. Pending, retryable,
846
+ non-delivered, and still-protected rows remain. There is no additional retention
847
+ configuration, timer, or scheduler.
417
848
 
418
849
  `BoundedContextBuilder.withDeliveryStrategy(strategy)` snapshots a validated
419
850
  immutable strategy for its Entity Inbox; the default is one shard. For example,
@@ -429,7 +860,6 @@ const context = BoundedContext.singleTenant("Tasks")
429
860
 
430
861
  Aggregate commands and Process Manager commands/events derive their target shard
431
862
  internally and persist their envelope in the target Entity Inbox before any
432
- handler runs. Delivery is never a post-request callback: local and remote
433
863
  handler runs. Local intake can directly drain the persisted Inbox in the
434
864
  current request path. Attached `ServerEnvironment` ports acknowledge admission
435
865
  and delivery workers replay it through the same path. Projection rows remain
@@ -443,12 +873,22 @@ before source client/storage. `DeliverySource` is not configured on
443
873
  durable supervisor state, topology failover, exactly-once effects, or automatic
444
874
  retry of unknown remote mutations.
445
875
 
446
- Each drain operation is bounded to one page. That bound limits a single
447
- operation, not the total pending work: an active lease owner can take another
448
- page while its policy retains the shard. The 30-second Inbox deduplication
449
- window controls duplicate admission only; it is not a replay-retention period.
450
- Persisted accepted rows follow their delivery lifecycle and may be replayed
451
- according to that lifecycle after the deduplication window has elapsed.
876
+ Each Inbox read is bounded to one page. That bound does not limit the total
877
+ pending work: an active drain continues from the last row read until that scan
878
+ ends. It restarts from the beginning only if the completed scan delivered
879
+ messages, allowing it to find work added during delivery. It finishes after a
880
+ scan delivers nothing; it does not restart after each productive page.
881
+ Delivery compares pending rows with delivered rows in the current page
882
+ and a process-local cache of the 1,000 most recent deliveries, then removes
883
+ duplicates. The 30-second Inbox deduplication window controls how long delivered
884
+ rows remain available as duplicate evidence; it is not a replay-retention
885
+ period. Persisted accepted rows follow their delivery lifecycle and may be
886
+ replayed according to that lifecycle after the deduplication window has elapsed.
887
+
888
+ Direct delivery uses the configured Inbox storage clock for both retained-row
889
+ duplicate checks and early cleanup filtering. Remote delivery uses local client
890
+ wall-clock time for these checks and does not assume it is synchronized with
891
+ the remote server.
452
892
 
453
893
  `DeliveryMonitor` is an instantiable, customizable policy seam exposing
454
894
  asynchronous continuation, start/completion, failed-reception, pickup-failure,
@@ -483,6 +923,15 @@ shard-update hints; when absent, the existing local source remains the fallback.
483
923
  Applications configure and close only the delivery facility through the
484
924
  environment: attachment supervisors and their source reads belong to that facility.
485
925
 
926
+ ## Signal identity and Aggregate semantics
927
+
928
+ Projection handlers cannot use `@Assign`; explicit metadata rejects that unsupported
929
+ handler shape. Framework-created child Commands and Events receive fresh IDs. Existing
930
+ Command or Event envelopes retain their IDs through transport and storage, including
931
+ when returned or passed as envelopes. Aggregate domain Events use the loaded pre-dispatch
932
+ version, while persisted Aggregate state advances to the next version. History caching accepts descending retained
933
+ versions with gaps, while continuing to reject equal or newer continuations.
934
+
486
935
  # Dynamic unary discovery
487
936
 
488
- `BrowserServerOptions.discovery` may supply changing complete application-node snapshots alongside fixed `backend` configuration. Both unary routing and native subscription streams use the same current membership. Empty membership retains durable definitions and later nodes reactivate them; a new backend-dependent subscription is unavailable until a node exists.
937
+ `BrowserServerOptions.discovery` from `@spine-event-engine/server/browser` may supply changing complete application-node snapshots alongside fixed `backend` configuration. Both unary routing and native subscription streams use the same current membership. Empty membership retains durable definitions and later nodes reactivate them; a new backend-dependent subscription is unavailable until a node exists.