@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/README.md CHANGED
@@ -1,569 +1,276 @@
1
1
  # Spine application server for Node.js
2
2
 
3
- This package runs a Spine bounded context in Node. It assembles entities,
4
- handlers, storage, command/event processing, query-side Projections,
5
- subscriptions, and a local Connect/gRPC-compatible server.
6
-
7
- ## Route a schema or interface token
8
-
9
- Use `.route(Schema, via)` for one message or `.route(Token, via)` for a named
10
- message-interface family. Exact schema routes take priority, then the first
11
- registered matching token, then replacement/default. The [To-Do example](../../examples/todo/USER_GUIDE.md)
12
- uses `TaskEvent`, an assignment token, and an exact `TaskReassigned` route.
13
-
14
- For a standalone application, use `await server.run()` to let the framework
15
- close the server on `SIGINT` or `SIGTERM` and permanently close the environment
16
- after the final run-managed server retires. Embedded applications use
17
- `await server.start()` and close the returned server themselves; `start()`
18
- never closes the shared environment. Active caller-managed and run-managed
19
- generations are exclusive, while matching `start()` or `run()` siblings share.
20
- Add the
21
- `browser` option with exact allowed origins and your session, authorization,
22
- and trusted-context collaborators to serve Connect and gRPC-Web without
23
- application listener or CORS code.
24
-
25
- ### Run complete replicas on one deployment node
26
-
27
- For a deployment node that uses several application processes, put the same
28
- application assembly in one ESM entry module and start it with
29
- `ManagedServerApplication.run()`. `processCount` is an explicit deployment
30
- setting. It is not inferred from CPU cores, and it has no relationship to the
31
- Delivery shard count chosen by the application.
3
+ `@spine-event-engine/server` is the native Node.js runtime for a Spine bounded
4
+ context. Use it to assemble contexts and expose native Connect/gRPC-compatible
5
+ services. Browser hosting is deliberately a separate entry point.
32
6
 
33
- ```ts
34
- import { ManagedServerApplication, type RunningServer } from "@spine-event-engine/server";
35
-
36
- declare function startCompleteApplicationReplica(endpoint: {
37
- host: string;
38
- port: number;
39
- }): Promise<RunningServer>;
40
-
41
- await ManagedServerApplication.run({
42
- processCount: 4,
43
- host: "0.0.0.0",
44
- port: 50051,
45
- moduleUrl: import.meta.url,
46
- createServer: startCompleteApplicationReplica,
47
- });
48
- ```
49
-
50
- `startCompleteApplicationReplica()` is application code: it configures the
51
- production `ServerEnvironment`, opens the application's Delivery facility,
52
- assembles every Bounded Context, adds them to `Server`, and starts that server
53
- at the supplied private endpoint. See the [To-Do replica assembly](../../examples/todo/src/multi-process-replica.ts) for a concrete implementation.
7
+ > **Experimental prerelease.** Install an exact snapshot while this API evolves:
8
+ > Snapshot releases may remove earlier prerelease import paths.
54
9
 
55
- When another host owns process signals, use the same options with `start()`
56
- and close the returned handle from that host:
10
+ ## Install and prerequisites
57
11
 
58
- ```ts
59
- import { ManagedServerApplication, type RunningServer } from "@spine-event-engine/server";
60
-
61
- declare function startCompleteApplicationReplica(endpoint: {
62
- host: string;
63
- port: number;
64
- }): Promise<RunningServer>;
65
-
66
- const managed = await ManagedServerApplication.start({
67
- processCount: 4,
68
- host: "0.0.0.0",
69
- port: 50051,
70
- moduleUrl: import.meta.url,
71
- createServer: startCompleteApplicationReplica,
72
- });
73
-
74
- await managed.close();
12
+ ```bash
13
+ pnpm add @spine-event-engine/server@snapshot
75
14
  ```
76
15
 
77
- The parent starts four separate children. Each child runs this same entry
78
- module, assembles the complete application, opens its own loopback listener,
79
- and completes its local readiness work before it is admitted. `run()` first
80
- waits for every configured child. Afterwards it stays ready while at least one
81
- child is ready; replacements continue while it is degraded, and it becomes
82
- unready if no child remains ready. A crashed child is replaced with bounded
83
- exponential delay; surviving children keep serving.
84
- The parent listens at the configured `host` and required nonzero `port` and
85
- forwards generated unary Command and Query gRPC calls to one READY child.
86
- The Coordinator fans each Gateway-owned logical subscription to the complete
87
- replicas and merges their updates. Each child observes the application's
88
- configured remote/shared Delivery facility directly; the parent does not proxy
89
- Delivery. Application assembly must select the Delivery facility and shard
90
- strategy appropriate to the deployment. The framework does not infer or
91
- certify their provenance, and managed process count neither detects CPU cores
92
- nor changes Delivery shard selection.
93
-
94
- Children progress `STARTING → SYNCHRONIZING → READY`. During synchronization,
95
- retained definitions are recreated and active streams attach before unary or
96
- Delivery admission; inactive definitions do not block readiness. Shutdown
97
- progresses `READY → DRAINING → CLOSED`: new unary and shard admission stops,
98
- active Delivery work may finish while subscription relays remain connected,
99
- then the child closes. The small parent/child channel carries only these
100
- lifecycle facts, including an opaque subscription-installation acknowledgement.
101
- It never carries Commands, Events, queries, subscription definitions or
102
- updates, or Delivery notifications. `ManagedServerApplication.run()` owns
103
- `SIGINT` and `SIGTERM` cleanup for its children. An embedding host uses
104
- `ManagedServerApplication.start()` instead and closes the returned managed
105
- handle itself; `start()` installs no process-signal handlers. Child listener
106
- topology remains private. Use direct `Server.run()` for one explicit local
107
- process or browser-oriented hosting.
108
-
109
- For detailed contracts intended for coding agents, see the
110
- [REFERENCE.md documentation for agents](REFERENCE.md).
111
-
112
- ## 💡 Why use it?
113
-
114
- - ✅ Builds bounded contexts with Aggregates, Process Managers, and query-side
115
- Projections.
116
- - ✅ Validates commands before application handlers run.
117
- - ✅ Exposes Command, Query, and Subscription services over native gRPC.
118
- - ✅ Optionally exposes authenticated Connect and gRPC-Web browser access.
119
- - ✅ Manages readiness, rollback, process signals, and ordered shutdown.
120
-
121
- ## 🧭 Understand the two event paths
122
-
123
- Each domain bounded context has an internal paired System Context. Domain events
124
- go to the domain `EventBus` and are durably appended to the domain `EventStore`.
125
- System events, such as committed entity-state notifications, go only to the
126
- System Context's `EventBus`; they never enter the domain EventStore. By default
127
- system events are forgotten after validation and notification. Call
128
- `persistSystemEvents()` when the application needs a separate optional System
129
- Context event store.
130
-
131
- Enable that option while assembling the context, before `build()` or
132
- `buildAsync()`:
133
-
134
- ```ts
135
- import { BoundedContext } from "@spine-event-engine/server";
136
-
137
- const context = BoundedContext.singleTenant("Tasks").persistSystemEvents().build();
138
- await context.close();
139
- ```
16
+ Use Node.js in ESM mode. Before adding entities, generate your application
17
+ model and handler registry with `@spine-event-engine/proto-tools`. The small
18
+ example below has no entity handlers, so it is useful for proving that native
19
+ assembly, listener startup, and cleanup work locally.
140
20
 
141
- `Stand` is the read side: it serves queries from current entity state. Exposed
142
- domain-event subscriptions observe domain events on the domain `EventBus`.
143
- Entity subscriptions observe `EntityStateChanged` on the paired System Context
144
- `EventBus` and turn those notifications into updates. A subscription is best
145
- effort, so clients use a query for initial state and recovery after reconnect
146
- or a possible gap.
21
+ ## First native server
147
22
 
148
- ## 🧱 Build a bounded context
23
+ Build one context, start a loopback listener on an ephemeral port, and close
24
+ the returned running server. `start()` installs no signal handlers, which makes
25
+ this shape appropriate for tests and hosts that manage shutdown themselves.
149
26
 
150
- Start with an explicit tenant mode. Register entity classes after their Proto
151
- model and generated handler registry are available.
27
+ <!-- docs-snippet-path: packages/server/test/server/server-environment.test.ts -->
152
28
 
153
29
  ```ts
154
- // docs-snippet-path: packages/server/test/context/bounded-context.test.ts
155
- import { BoundedContext } from "@spine-event-engine/server";
156
-
157
- const context = await BoundedContext.singleTenant("Tasks").buildAsync();
158
- await context.close();
30
+ import { BoundedContext, Server } from "@spine-event-engine/server";
31
+
32
+ const context = BoundedContext.singleTenant("Tasks").build();
33
+ const running = await new Server({ contexts: [context], port: 0 }).start();
34
+ try {
35
+ // Native Command, Query, and Subscription services are now listening.
36
+ } finally {
37
+ await running.close();
38
+ }
159
39
  ```
160
40
 
161
- Use `withStorageFactory()` to choose storage. Use
162
- `withGeneratedRegistryRoot()` for framework-generated repository assembly, or
163
- register explicitly assembled repositories with `add()`. `buildAsync()` is the
164
- normal choice for an application that uses generated handlers.
41
+ Add generated entities with `BoundedContext.add(...)` and use
42
+ `buildAsync()` when framework-generated repository discovery is needed. A
43
+ context is single-tenant or multitenant by construction; select that mode
44
+ before registering application handlers and storage.
165
45
 
166
- ### Keep Stand subscription definitions
46
+ ## Give an Entity an application service
167
47
 
168
- Stand subscription definitions use your context storage by default. One
169
- `spine.client.SubscriptionRecord` is stored for each explicit subscription ID.
170
- Pending definitions expire if they are not activated; reconciliation is
171
- best-effort at a ten-second interval, so subscription updates are not complete
172
- delivery and clients re-query after reconnects or gaps.
48
+ Spine normally constructs an Entity with its ID, state, version, and lifecycle
49
+ options. If your constructor also needs a service, supply `onCreate` when
50
+ registering the class. For example, the Projects application's Process Manager
51
+ uses an assignment-weight service:
173
52
 
174
- The storage record exposes the physical columns `status` and
175
- `when_activation_expires`. Cleanup asks the provider for 26 pending records,
176
- ordered by `when_activation_expires` and then ID. It deletes at most 25 expired
177
- records and reports more work only when the observed 26th record is expired.
178
-
179
- For an application-specific registry, provide one complete implementation.
180
- The built context closes it.
53
+ <!-- docs-snippet-path: examples/projects/test/topology.test.ts -->
181
54
 
182
55
  ```ts
183
- import { BoundedContext, type StandSubscriptionRegistry } from "@spine-event-engine/server";
184
-
185
- declare const registry: StandSubscriptionRegistry;
186
-
187
- const context = BoundedContext.singleTenant("Tasks").withSubscriptionRegistry(registry).build();
56
+ import { BoundedContext } from "@spine-event-engine/server";
57
+ import { TaskAssignment, type AssignmentWeightService } from "../src/index.js";
58
+
59
+ // Create shared services before starting the context.
60
+ const weights: AssignmentWeightService = { weightFor: () => 2 };
61
+ const builder = BoundedContext.singleTenant("Assignments")
62
+ .withGeneratedRegistryRoot(new URL("..", import.meta.url))
63
+ .add(TaskAssignment, {
64
+ // Forward Spine's options and add the service required by the constructor.
65
+ onCreate: (options) => new TaskAssignment(options, weights),
66
+ });
188
67
  ```
189
68
 
190
- `InMemorySubscriptionRegistry` is useful in local development and tests. When a
191
- context using it is attached to a production `ServerEnvironment`, it emits one
192
- warning naming the context because definitions disappear on restart; startup
193
- still continues.
194
-
195
- ### Managed subscription fan-out
196
-
197
- `SubscriptionService` remains a best-effort live stream: it has no replay or
198
- completeness promise. In standalone mode, its default registry is persistent.
199
- In managed complete-replica mode, the framework owns every child `Server` and
200
- requires each actual Bounded Context registry to be exactly an
201
- `InMemorySubscriptionRegistry`; persistent and custom registries are rejected
202
- and the assembled child is closed before READY.
203
-
204
- The Gateway alone owns durable logical subscription bindings. It rehydrates
205
- them after restart, while its Coordinator fans a definition out to READY
206
- replicas, merges their live updates, and propagates cancellation. Definition
207
- payloads never cross parent/child IPC. Late or replacement replicas first
208
- synchronize retained definitions and only then become eligible for ordinary
209
- traffic. Real-process event and Delivery handoff verification is documented
210
- separately from this server configuration guide.
211
-
212
- ### Follow one definition from creation to cleanup
213
-
214
- The registry stores a definition first, then makes it active. Applications use
215
- the generated `SubscriptionId` throughout the lifecycle. `get()` and
216
- `snapshot()` return cloned values, so treat them as observations rather than
217
- mutable working objects. Their Protobuf byte arrays remain mutable, but do not
218
- alias stored or caller bytes.
69
+ Call `builder.buildAsync()` to assemble the context. The callback runs for each
70
+ fresh or restored Entity object, not just once per ID. It also works for
71
+ Aggregates and Projections, including Projection rebuilds. Return a fresh
72
+ instance synchronously and pass the supplied options unchanged. Application
73
+ services are not persisted; the application manages their initialization and
74
+ shutdown. Constructors that need only `EntityOptions` require no callback.
75
+
76
+ Explicit repositories accept `onCreate` in their options too. See the
77
+ [guide](https://github.com/SpineEventEngine/spine-ts/blob/master/docs/USER_GUIDE.md#give-an-entity-an-application-service) for the
78
+ full story and the [reference](REFERENCE.md#entity-constructor-dependencies)
79
+ for the exact contract.
80
+
81
+ ## Async handlers and Process Manager queries
82
+
83
+ An `@Assign`, `@Command`, `@React`, or `@Subscribe` handler may return its
84
+ usual result directly or through exactly one built-in `Promise<T>` layer. Its
85
+ transaction remains open until that promise settles. Nested `Promise<Promise<T>>`
86
+ results and structural or imported thenable lookalikes are rejected during handler
87
+ analysis. Rejection rolls back framework state and suppresses produced output; it
88
+ cannot roll back an external HTTP request or other side effect.
89
+
90
+ Handler return declarations can use native unions for one of several results,
91
+ or fixed tuples for several ordered results. For example,
92
+ `CreateAccessGrant | ExtendAccessGrant` selects one Command, while
93
+ `readonly [AccessGrantCreated, AccessRequestCompleted?]` returns an Event and
94
+ possibly a second Event. Each actual result must be declared by that handler.
95
+ Arrays and local/imported aliases of these concrete message types are supported
96
+ too. `@Assign` returns Events, `@Command` returns Commands, `@React` returns
97
+ Events or `undefined`, and `@Subscribe` declares `void` or `Promise<void>`.
98
+ Event/rejection-input `@Command` also permits `undefined`, alone or alongside
99
+ concrete Command types. Only subscriptions accept `void`; required empty
100
+ results and unexpected returned values fail before Entity commit or publication.
101
+ See the [return-type guide](https://github.com/SpineEventEngine/spine-ts/blob/master/docs/USER_GUIDE.md#choose-what-a-handler-returns)
102
+ for examples, optional results, and the restrictions for each decorator.
103
+
104
+ Every Entity's `version` is the generated Spine `Version`; the framework
105
+ advances it once when a successful dispatch produces Events or changes state or
106
+ lifecycle, not for a no-op.
107
+
108
+ Declare each domain rejection a command handler may throw. This registers the
109
+ rejection before the server starts, so a client can subscribe even when no
110
+ server handler consumes that rejection:
111
+
112
+ <!-- docs-snippet-path: examples/todo/src/index.ts -->
219
113
 
220
114
  ```ts
221
- import { create } from "@bufbuild/protobuf";
222
- import { InMemorySubscriptionRegistry } from "@spine-event-engine/server";
223
- import { SubscriptionIdSchema, SubscriptionSchema } from "@spine-event-engine/proto/client";
224
-
225
- const registry = new InMemorySubscriptionRegistry();
226
- const id = create(SubscriptionIdSchema, { value: "daily-report" });
227
- const definition = create(SubscriptionSchema, {
228
- id,
229
- topic: { id: { value: "reports.daily" } },
230
- });
231
-
232
- await registry.create(definition); // pending for up to 30 seconds
233
- await registry.activate(id); // active definitions do not expire
234
- const current = await registry.get(id); // one isolated snapshot
235
- const all = await registry.snapshot(); // complete, identifier-sorted copies
236
- if (current !== undefined) await registry.delete(id);
237
- await registry.cleanup(); // removes at most 25 expired pending entries
238
- await registry.close();
239
-
240
- void all;
115
+ import { Assign, Throws } from "@spine-event-engine/server";
116
+ import type { CompleteTask } from "../generated/spine/examples/todo/task_commands_pb.js";
117
+ import type { TaskCompleted } from "../generated/spine/examples/todo/task_events_pb.js";
118
+ import { TaskAlreadyDone } from "../generated/spine/examples/todo/task_rejections.js";
119
+
120
+ class TaskAssignee {
121
+ @Assign
122
+ @Throws(TaskAlreadyDone)
123
+ completeTask(command: CompleteTask): TaskCompleted {
124
+ throw TaskAlreadyDone.create({ id: command.id });
125
+ }
126
+ }
241
127
  ```
242
128
 
243
- The built-in durable registry requires record storage with atomic
244
- compare-and-set. Context construction fails fast when the selected storage
245
- provider cannot supply that capability; it never silently falls back to memory.
129
+ Place the primary handler decorator first for readability. Reversing the two
130
+ decorators has the same behavior.
246
131
 
247
- Provider configuration for `SubscriptionRecord` applies to these definitions.
248
- For example, a MySQL factory can assign `SubscriptionRecordSchema` to a chosen
249
- table, and a Datastore factory can provide storage for that same schema. The
250
- registry uses the configured provider when the context is built.
251
-
252
- An aggregate receives a generated command type in an `@Assign` method. The
253
- generator discovers this method and writes the registry used by `buildAsync()`;
254
- run `spine-proto handlers` after changing it.
132
+ Process Managers, but not Aggregates, have protected read-only `select()` during a handler:
255
133
 
256
134
  ```ts
257
- // docs-snippet-path: examples/orders/src/index.ts
258
- import { create } from "@bufbuild/protobuf";
259
- import { Aggregate, Assign } from "@spine-event-engine/server";
260
- import {
261
- CreateOrderSchema,
262
- type CreateOrder,
263
- } from "../generated/spine/examples/orders/commands_pb.js";
264
- import { OrderSchema } from "../generated/spine/examples/orders/entities_pb.js";
265
- import {
266
- OrderCreatedSchema,
267
- type OrderCreated,
268
- } from "../generated/spine/examples/orders/events_pb.js";
269
-
270
- class OrderAggregate extends Aggregate<string, typeof OrderSchema> {
271
- @Assign createOrder(command: CreateOrder): OrderCreated {
272
- this.update((draft) =>
273
- Object.assign(draft, create(OrderSchema, { id: this.id, skuId: command.skuId })),
274
- );
275
- return create(OrderCreatedSchema, { id: this.id, skuId: command.skuId });
135
+ import { EntityQuery, type EntityColumn } from "@spine-event-engine/core";
136
+ import { ProcessManager } from "@spine-event-engine/server";
137
+
138
+ type RequestView = import("@bufbuild/protobuf").Message<"RequestView"> & {
139
+ id: string;
140
+ status: string;
141
+ createdAt: number;
142
+ };
143
+ declare const RequestViewSchema: import("@bufbuild/protobuf/codegenv2").GenMessage<RequestView>;
144
+ declare const RequestViewColumns: {
145
+ readonly status: EntityColumn<typeof RequestViewSchema, "status", string, "equal">;
146
+ readonly createdAt: EntityColumn<
147
+ typeof RequestViewSchema,
148
+ "createdAt",
149
+ number,
150
+ "equal" | "greaterThan"
151
+ >;
152
+ };
153
+ abstract class RequestCoordinator extends ProcessManager<string, typeof RequestViewSchema> {
154
+ protected async handleRequest(requestId: string): Promise<void> {
155
+ const pending = await this.select(RequestViewSchema, RequestViewColumns)
156
+ .where(EntityQuery.eq(RequestViewColumns.status, "pending"))
157
+ .orderBy(RequestViewColumns.createdAt, "asc")
158
+ .limit(10)
159
+ .read();
160
+ const one = await this.select(RequestViewSchema, RequestViewColumns).findById(requestId);
161
+ void [pending, one];
276
162
  }
277
163
  }
278
164
  ```
279
165
 
280
- ## 🚀 Run a standalone server
281
-
282
- `Server` exposes the supplied built contexts through the Spine command, query,
283
- and subscription services. Its default host is local-only.
284
-
285
- ```ts
286
- // docs-snippet-path: packages/server/src/server/server.ts
287
- import { Server, type BoundedContext } from "@spine-event-engine/server";
166
+ These reads are eventually consistent. `limit()` may not exceed 1,000, and
167
+ `all()` can be expensive for a large read model; prefer a targeted, ordered,
168
+ bounded query.
169
+
170
+ The Entity type identifies its context among those registered with the same
171
+ `Server`. For example, an order process can query a product in a catalogue
172
+ context without naming that context. An Entity in another context must allow
173
+ queries (`query` or `full` visibility). Registering the same Entity type in two
174
+ contexts causes a startup error.
175
+
176
+ The [Orders example](https://github.com/SpineEventEngine/spine-ts/blob/master/examples/orders/README.md#cross-context-order-review)
177
+ shows this in an Event handler and includes a runnable integration test.
178
+
179
+ Queries preserve the handler's actor and effective tenant; there is no tenant
180
+ override. Single-tenant execution uses `SINGLE_TENANT`, even when the request
181
+ omits a tenant field. A named tenant cannot query a single-tenant context: that
182
+ fails before reading. See the [query contract](REFERENCE.md#handler-routing-and-operations)
183
+ for the complete tenant rules and empty-result behavior.
184
+
185
+ Use `EntityQuery.all(...)` when every predicate must match and
186
+ `EntityQuery.either(...)` when any branch may match.
187
+
188
+ ## Native lifecycle and routing
189
+
190
+ `Server.run()` is the standalone process form: it handles `SIGINT`/`SIGTERM`
191
+ cleanup. `Server.start()` returns a `RunningServer` for an embedding host to
192
+ close. `ManagedServerApplication` is the Node-only complete-replica entrypoint;
193
+ it does not infer process or Delivery-shard counts.
194
+
195
+ Routes are declared by generated message schema or interface token. Exact
196
+ schema routes take precedence. Query results from `Stand` are authoritative;
197
+ subscriptions are live, best-effort notifications, so clients re-query after a
198
+ reconnect or a possible gap. Configure a storage provider for durable
199
+ application state and generated repository assembly; in-memory choices are
200
+ for local development and tests.
201
+
202
+ ## Entity identifiers
203
+
204
+ An Entity ID can be a primitive value or a complete generated Protobuf message.
205
+ For a message-valued ID, the Entity state message declares that ID message as
206
+ its first field. Every field declared by the ID message, including nested
207
+ messages, is part of the identity.
208
+
209
+ ```proto
210
+ message WorkItemId {
211
+ spine.core.UserId owner = 1;
212
+ int32 sequence = 2;
213
+ }
288
214
 
289
- declare const context: BoundedContext;
290
- const server = new Server({ contexts: [context], port: 0 });
291
- const running = await server.run();
292
- console.log(running.baseUrl);
215
+ message WorkItem {
216
+ WorkItemId id = 1;
217
+ string title = 2;
218
+ }
293
219
  ```
294
220
 
295
- Use `run()` for a standalone process. It installs framework `SIGINT` and
296
- `SIGTERM` shutdown; siblings share its generation, and a failed final close is
297
- retryable by a later signal or `close()`. Use `start()` when another host handles
298
- the process lifecycle; it shares only caller-managed siblings and never closes
299
- the environment.
221
+ Generated messages can have any declared identifier fields. For example,
222
+ `CommandId` is a generated message ID:
300
223
 
301
224
  ```ts
302
- import { Server, type BoundedContext } from "@spine-event-engine/server";
225
+ import { create } from "@bufbuild/protobuf";
226
+ import { type MessageId } from "@spine-event-engine/server";
227
+ import { CommandIdSchema } from "@spine-event-engine/proto";
303
228
 
304
- declare const context: BoundedContext;
305
- const running = await new Server({ contexts: [context], port: 8080 }).run();
306
- console.log(`Spine server ready at ${running.baseUrl}`);
229
+ const id = create(CommandIdSchema, { uuid: "task-42" });
230
+ const entityId: MessageId = id;
307
231
  ```
308
232
 
309
- ## Delivery and subscriptions
310
-
311
- An `@Assign` command for an Aggregate or Process Manager first persists in that
312
- Entity's Inbox. Local intake can then directly drain the persisted work in the
313
- current request path. With an attached `ServerEnvironment`, delivery workers
314
- replay admitted Inbox work through the same path. In shared remote delivery,
315
- every matching node may attempt a shard, while one active lease owner performs
316
- one bounded drain page; later drains can have a different lease owner. A page
317
- bound does not cap all pending work: the owner may take later pages until the
318
- delivery policy releases the shard. Events and later Process Manager commands
319
- use the same path. See [REFERENCE.md](REFERENCE.md) for operational delivery
320
- details.
321
-
322
- Domain-event subscriptions observe exposed domain events on the domain
323
- `EventBus`. Entity subscriptions observe committed `EntityStateChanged`
324
- notifications on the paired System Context `EventBus`. Stand's default registry
325
- uses the application's `StorageFactory`; a builder may supply another
326
- implementation. A definition is pending for at most 30 seconds, active
327
- definitions have no framework TTL, and cancellation physically deletes the
328
- definition. Nodes reconcile their local listeners from a complete
329
- snapshot every 10 seconds. Active streams and queues are process-local.
330
-
331
- ### Serve browser clients
332
-
333
- Add `browser` configuration to expose Connect and gRPC-Web without writing a
334
- listener, router, CORS middleware, or shutdown coordinator. The application
335
- still supplies its authorization policy and trusted actor context resolver.
336
- Choose exactly one admission mode:
337
-
338
- - supply `sessions` when browser callers must authenticate; or
339
- - set `publicAccess: true` for an intentionally public endpoint with no login
340
- session.
341
-
342
- Never supply both. Public access does not manufacture a session or an expiry.
343
- The application policy must still authorize every operation and rebuild trusted
344
- actor and tenant context from request facts.
345
-
346
- For a separately hosted TS or JVM backend, set `browser.backend.baseUrl` to its
347
- canonical HTTP(S) origin. The gateway keeps `ResolveContext` local and forwards
348
- the five application RPCs only after the configured admission, authorization,
349
- and trusted-context rewrite. An authenticated standalone mode requires explicit
350
- subscription bindings: local development and tests may explicitly supply an
351
- in-memory binding, while production also needs a type registry and named
352
- `DurableSubscriptionBindings`. A public standalone mode instead owns
353
- process-local bindings. A Gateway restart drops those definitions, so browsers
354
- reconnect, query authoritative state, and subscribe again. The external backend
355
- remains under caller control.
356
-
357
- Use `browser.backend.baseUrls` for non-empty unique origins. Unary calls use
358
- round-robin without retry; native streams fan out best-effort, so clients
359
- re-query authoritative state after duplicate updates or a generic loss notice.
360
-
361
- Applications may add only explicit OAuth-style callbacks with `authRoutes`.
362
- Each route uses an exact `GET`/`POST` path, per-route origins, a safe body limit,
363
- and a finite timeout. This is not a general HTTP router; use it for bounded
364
- identity exchanges and validate OAuth state in the application handler.
365
- The host admits at most 64 active auth requests by default (or the configured
366
- positive `maxActiveAuthRequests`); excess requests receive 503 before a handler
367
- runs. A route timeout covers request intake, handler work, and response
368
- transfer. Auth response bodies are bounded by `writeMaxBytes`; an overflow
369
- returns 413 without copying application headers.
233
+ At routing, the generated ID is checked against the state field's declared
234
+ schema and its generated validation rules before durable use. When default
235
+ routing finds a message but the Entity ID type is primitive, define a custom
236
+ route that returns the intended primitive ID.
370
237
 
371
- ```ts
372
- import {
373
- DurableSubscriptionBindings,
374
- Server,
375
- type BoundedContext,
376
- } from "@spine-event-engine/server";
377
-
378
- declare const context: BoundedContext;
379
- declare const sessions: import("@spine-event-engine/auth").SessionResolver;
380
- declare const authorize: import("@spine-event-engine/auth").AuthorizationPolicy["authorize"];
381
- declare const contextResolver: import("@spine-event-engine/auth").ContextResolver;
382
- declare const clock: import("@spine-event-engine/auth").Clock;
383
- declare const registryStorage: import("@spine-event-engine/storage").StorageFactory;
384
-
385
- const bindings = new DurableSubscriptionBindings({
386
- storageFactory: registryStorage,
387
- namespace: "my-app",
388
- nextId: () => crypto.randomUUID(),
389
- cleanup: async () => undefined,
390
- });
391
-
392
- const running = await new Server({
393
- contexts: [context],
394
- port: 8090,
395
- browser: {
396
- origins: ["http://127.0.0.1:5173"],
397
- sessions,
398
- authorize,
399
- contexts: contextResolver,
400
- clock,
401
- bindings,
402
- },
403
- }).run();
404
-
405
- void running;
406
- ```
238
+ Without a custom route, Command routing uses the Command's declared first
239
+ field. Event routing uses a compatible producer ID when available and otherwise
240
+ uses the Event's declared first field. State-update routing selects the first
241
+ state field compatible with the Entity ID.
407
242
 
408
- For an intentionally public browser application, replace `sessions` and
409
- `bindings` in the example with `publicAccess: true`. Do not supply both modes.
410
- Public mode uses process-local bindings and has no synthetic session expiry.
411
-
412
- In production, authenticated browser access requires
413
- `DurableSubscriptionBindings`. Startup rejects missing or in-memory bindings
414
- before opening a listener. The registry uses the storage factory that your
415
- application supplies and closes only the handle, so you can use a separate
416
- factory from application-data storage or intentionally share one. Its namespace
417
- separates applications sharing a provider. Combined authenticated browser mode
418
- may omit bindings to use an in-memory registry; authenticated standalone mode
419
- must always supply them explicitly.
420
-
421
- Each durable binding stores one approved `GatewayAuthenticatedSubscription`:
422
- its public ID, complete subscription, and expiry. The stored Topic retains the
423
- trusted Actor and Tenant, so Activate and Cancel must match that context.
424
-
425
- - Cancellation cleans up the backend before deleting the record; a failure
426
- leaves it available for retry.
427
- - Request-time cleanup is finite; a maintenance loop may call
428
- `purgeExpired(nowMs)`.
429
- - Restart rehydrates unexpired definitions. An expired definition is removed
430
- only after cleanup succeeds.
431
-
432
- This is a single-Gateway persistence model. It has no multi-process
433
- coordination, quotas, reservations, or durable fingerprints. Configure MySQL
434
- with `setTableName(GatewayAuthenticatedSubscriptionSchema, table)` or Datastore
435
- with `useRecordStorage(GatewayAuthenticatedSubscriptionSchema, creator)`.
436
-
437
- The server validates commands before handler code runs. Invalid payloads are
438
- returned as `COMMAND_VALIDATION_ERROR`; invalid state transitions are returned
439
- as `COMMAND_STATE_TRANSITION_VALIDATION_FAILED`. Domain rejections roll back
440
- the entity change and are posted independently as typed rejection events.
441
-
442
- ## 🔎 Read state and updates
443
-
444
- `context.stand()` provides framework-managed current entity state reads and
445
- in-process update subscriptions. `SpineServices` translates the standard query
446
- and subscription service requests for registered state routes.
243
+ ## Browser gateway migration
447
244
 
448
- ```ts
449
- // docs-snippet-path: packages/server/test/context/bounded-context.test.ts
450
- import type { BoundedContext } from "@spine-event-engine/server";
245
+ The server root has no browser options and does not load auth. To host a
246
+ browser-facing Gateway, install the optional auth peer and import the browser
247
+ entry point explicitly:
451
248
 
452
- declare const context: BoundedContext;
453
- const stand = context.stand();
454
- void stand;
249
+ ```bash
250
+ pnpm add @spine-event-engine/server@snapshot @spine-event-engine/auth@snapshot
455
251
  ```
456
252
 
457
- Subscriptions and active queues are local-process resources. A client must
458
- re-query after reconnect; this package does not promise cluster-complete
459
- updates, event replay for subscriptions, or exactly-once observation.
460
-
461
- ## 🌐 Delivery and environment
462
-
463
- `DeliveryBuilder` builds a delivery from its inbox, work registry, shard, and
464
- page-size choices. `DeliverySupervisor({ source, delivery, onMessage })` receives
465
- the separate public `DeliverySource` used to observe remote shards.
466
- `Environment` resolves the Node deployment profile; the singleton
467
- `ServerEnvironment` exposes configured storage, optional delivery/tracing
468
- facilities, and its process lifecycle. They do not create a production topology
469
- or durable scheduler for you.
470
-
471
- `DeliveryMonitor` is the customizable explicit failure-policy seam. Its hooks
472
- may return a value or a promise; a failed reception defaults to durable
473
- `markDelivered()` and continues independent targets. Applications may instead
474
- select one immediate `repeatDispatching()` action. The monitor adds no attempts,
475
- quarantine, receipts, markers, timers, backoff, dead-letter storage, or
476
- scheduler policy. Each delivery lifetime has an opaque `WorkerId`; graceful
477
- stop waits for admitted work to settle before releasing its shard session.
478
-
479
- The default Entity Inbox has one shard. Configure more shards through the public
480
- builder chain:
481
-
482
253
  ```ts
483
- import { BoundedContext, UniformAcrossAllShards } from "@spine-event-engine/server";
484
-
485
- const context = BoundedContext.singleTenant("Tasks")
486
- .withDeliveryStrategy(UniformAcrossAllShards.forNumber(3))
487
- .build();
254
+ import { BrowserServer, DurableSubscriptionBindings } from "@spine-event-engine/server/browser";
488
255
  ```
489
256
 
490
- Aggregate commands and Process Manager commands/events
491
- derive a target shard themselves and persist before handler replay. Local delivery
492
- drains in the posting request; after `ServerEnvironment` supplies delivery ports,
493
- posting persists and acknowledges while a worker replays later. Projection delivery
494
- is separate.
495
-
496
- An openable `ServerEnvironmentDelivery` is opened before the first environment
497
- attachment is admitted, then supplies its inbox and work-registry ports to both
498
- the existing finite and supervisor delivery paths; a close-only local delivery
499
- remains supported. The environment stops attachments before closing delivery,
500
- tracing, and storage in that order. Use `RemoteDelivery` from
501
- `@spine-event-engine/delivery-client` when an application selects a remote
502
- delivery endpoint with direct authoritative removal.
503
-
504
- Shard ownership excludes concurrent delivery within a shard. Pending and
505
- delivered Inbox rows are stored directly, and delivered rows are the
506
- deduplication fact. Handler effects and the delivered-row compare-and-set are
507
- not transactional: a lost acknowledgement can redeliver after restart, so
508
- downstream handling must be idempotent.
509
-
510
- ## ⚠️ Runtime boundaries
511
-
512
- The package is Node-only. It does not choose an identity provider, session
513
- database, production storage, TLS setup, or durable multi-machine delivery.
514
- Subscription updates are notices rather than complete history, so clients
515
- re-query entity state after reconnecting.
516
-
517
- ## 🔗 Learn more
518
-
519
- - [End-user guide](../../docs/USER_GUIDE.md)
520
- - [Authentication](../auth/README.md)
521
- - [Node client](../client-node/README.md)
522
- - [Browser client](../client-web/README.md)
523
- - [Message Board application](../../examples/message-board/README.md)
524
- - [Reference for coding agents](REFERENCE.md)
525
-
526
- ## Cross-context external events
527
-
528
- Every built `BoundedContext` owns one private integration broker. It publishes
529
- domestic events only when another context has requested them, and an imported
530
- event is delivered only to handlers declared with `External<T>`. The marker is
531
- type-only and transparent at runtime:
257
+ `BrowserServer.open(...)` composes a browser listener around a caller-managed
258
+ native server; `BrowserServer.run(...)` starts and manages a standalone or
259
+ combined browser host. Supply exact allowed origins, a session or intentional
260
+ public admission, authorization and trusted-context resolvers, finite limits,
261
+ and backend forwarding. Authenticated durable bindings are browser-host
262
+ infrastructure, not native `Server` state. See the [browser/auth extension guide](https://github.com/SpineEventEngine/spine-ts/blob/master/docs/BROWSER_CLIENT_AUTH_EXTENSION_GUIDE.md)
263
+ before exposing a backend to a browser.
532
264
 
533
- ```ts
534
- // docs-snippet-path: examples/todo/src/index.ts
535
- import { Subscribe, type External } from "@spine-event-engine/server";
536
- import type { TaskCreated } from "../generated/spine/examples/todo/task_events_pb.js";
537
-
538
- class TaskProjection {
539
- @Subscribe
540
- onImported(event: External<TaskCreated>): void {
541
- void event;
542
- }
543
- }
544
- ```
265
+ ## Limits
266
+
267
+ This package does not create an identity provider, browser session store,
268
+ authorization policy, TLS configuration, or deployment topology. It does not
269
+ guarantee replay, gap repair, exactly-once subscription effects, or
270
+ cluster-complete notification delivery.
271
+
272
+ ## Next steps
545
273
 
546
- `External<T>` must be the direct first receptor parameter. External commands
547
- are invalid; external events, rejections, and supported state subscriptions
548
- are valid. A context publishes only the event types requested by interested
549
- contexts. This requested-only export, plus domestic/external filtering, is the
550
- loop-prevention rule: there is no hop counter, broker Inbox, retry, replay,
551
- deduplication, or producer election. Delivery is best effort; many consumers
552
- may observe one domain producer at a time.
553
-
554
- The broker uses private `TransportFactory` message channels.
555
- `ServerEnvironment` owns one process-wide `InMemoryTransportFactory` by
556
- default for private IntegrationBroker message channels, including production.
557
- An application may supply `integrationChannelFactory` to override that local
558
- channel factory. Production resolution still requires `storageFactory` and the
559
- complete application `typeRegistry`; it does not fall back to in-memory storage
560
- or to the core-only registry. No application signal-binding setting is part of
561
- the environment. The channel factory is private IntegrationBroker plumbing,
562
- not a general runtime transport.
563
-
564
- `ThirdPartyContext.singleTenant(name)` or `.multitenant(name)` creates the
565
- public import facade. `emittedEvent(event, actor)` requires a generated event,
566
- preserves the actor tenant policy, and uses the actor timestamp (or fills one
567
- for a `UserId`). An unknown local event schema rejects before publication; a
568
- valid event with no current interested context is a successful no-op. Close the
569
- hidden context when imports are finished.
274
+ - [Detailed coding-agent reference](REFERENCE.md)
275
+ - [Authentication gateway](https://github.com/SpineEventEngine/spine-ts/blob/master/packages/auth/README.md)
276
+ - [Message Board application](https://github.com/SpineEventEngine/spine-ts/blob/master/examples/message-board/app/README.md)