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

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 (97) hide show
  1. package/README.md +63 -539
  2. package/REFERENCE.md +32 -10
  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/context/bounded-context.d.ts.map +1 -1
  18. package/dist/context/bounded-context.js +2 -1
  19. package/dist/context/bounded-context.js.map +1 -1
  20. package/dist/context/entity-inbox.js +1 -1
  21. package/dist/context/entity-inbox.js.map +1 -1
  22. package/dist/context/local-inbox-handoff.js +1 -1
  23. package/dist/context/local-inbox-handoff.js.map +1 -1
  24. package/dist/context/tenant-index.d.ts.map +1 -1
  25. package/dist/context/tenant-index.js +2 -1
  26. package/dist/context/tenant-index.js.map +1 -1
  27. package/dist/delivery/inbox-storage.js +1 -1
  28. package/dist/delivery/inbox-storage.js.map +1 -1
  29. package/dist/delivery/sharded-work-registry.d.ts.map +1 -1
  30. package/dist/delivery/sharded-work-registry.js +2 -1
  31. package/dist/delivery/sharded-work-registry.js.map +1 -1
  32. package/dist/entity/entity-storage-descriptor.d.ts +1 -1
  33. package/dist/entity/entity-storage-descriptor.d.ts.map +1 -1
  34. package/dist/handler/generated-handler-registry.d.ts +0 -7
  35. package/dist/handler/generated-handler-registry.d.ts.map +1 -1
  36. package/dist/handler/generated-handler-registry.js.map +1 -1
  37. package/dist/index.d.ts +1 -2
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +0 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/repository/repository.d.ts.map +1 -1
  42. package/dist/repository/repository.js +3 -2
  43. package/dist/repository/repository.js.map +1 -1
  44. package/dist/server/environment-attachment.d.ts.map +1 -1
  45. package/dist/server/environment-attachment.js +2 -1
  46. package/dist/server/environment-attachment.js.map +1 -1
  47. package/dist/server/environment-delivery-worker.d.ts.map +1 -1
  48. package/dist/server/environment-delivery-worker.js +2 -1
  49. package/dist/server/environment-delivery-worker.js.map +1 -1
  50. package/dist/server/node-coordinator.js +2 -2
  51. package/dist/server/node-coordinator.js.map +1 -1
  52. package/dist/server/server.d.ts +9 -177
  53. package/dist/server/server.d.ts.map +1 -1
  54. package/dist/server/server.js +24 -72
  55. package/dist/server/server.js.map +1 -1
  56. package/dist/services/spine-services.js +2 -1
  57. package/dist/services/spine-services.js.map +1 -1
  58. package/dist/spi/delivery.d.ts +5 -0
  59. package/dist/spi/delivery.d.ts.map +1 -0
  60. package/dist/spi/delivery.js +18 -0
  61. package/dist/spi/delivery.js.map +1 -0
  62. package/dist/spi/handler-registry.d.ts +8 -0
  63. package/dist/spi/handler-registry.d.ts.map +1 -0
  64. package/dist/spi/handler-registry.js +15 -0
  65. package/dist/spi/handler-registry.js.map +1 -0
  66. package/dist/stand/stand.d.ts.map +1 -1
  67. package/dist/stand/stand.js +2 -1
  68. package/dist/stand/stand.js.map +1 -1
  69. package/dist/stand/subscription-observer.js +1 -1
  70. package/dist/stand/subscription-observer.js.map +1 -1
  71. package/dist/stand/subscription-registry.js +1 -1
  72. package/dist/stand/subscription-registry.js.map +1 -1
  73. package/dist/testing/index.d.ts +2 -3
  74. package/dist/testing/index.d.ts.map +1 -1
  75. package/dist/testing/index.js +2 -3
  76. package/dist/testing/index.js.map +1 -1
  77. package/dist/tsconfig.tsbuildinfo +1 -1
  78. package/package.json +34 -23
  79. package/dist/handler/build-time-handler-analyzer.d.ts +0 -150
  80. package/dist/handler/build-time-handler-analyzer.d.ts.map +0 -1
  81. package/dist/handler/build-time-handler-analyzer.js +0 -1101
  82. package/dist/handler/build-time-handler-analyzer.js.map +0 -1
  83. package/dist/handler/generated-registry-writer.d.ts +0 -58
  84. package/dist/handler/generated-registry-writer.d.ts.map +0 -1
  85. package/dist/handler/generated-registry-writer.js +0 -458
  86. package/dist/handler/generated-registry-writer.js.map +0 -1
  87. package/dist/handler/handler-codegen.d.ts +0 -48
  88. package/dist/handler/handler-codegen.d.ts.map +0 -1
  89. package/dist/handler/handler-codegen.js +0 -214
  90. package/dist/handler/handler-codegen.js.map +0 -1
  91. package/dist/server/browser-server.d.ts +0 -49
  92. package/dist/server/browser-server.d.ts.map +0 -1
  93. package/dist/server/browser-server.js.map +0 -1
  94. package/dist/server/durable-subscription-bindings.d.ts.map +0 -1
  95. package/dist/server/durable-subscription-bindings.js.map +0 -1
  96. /package/dist/{server → browser}/durable-subscription-bindings.d.ts +0 -0
  97. /package/dist/{server → browser}/durable-subscription-bindings.js +0 -0
package/README.md CHANGED
@@ -1,569 +1,93 @@
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.
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.
6
6
 
7
- ## Route a schema or interface token
7
+ > **Experimental prerelease.** Install an exact snapshot while this API evolves:
8
+ > Snapshot releases may remove earlier prerelease import paths.
8
9
 
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.
10
+ ## Install and prerequisites
13
11
 
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.
32
-
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.
54
-
55
- When another host owns process signals, use the same options with `start()`
56
- and close the returned handle from that host:
57
-
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.
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.
93
20
 
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.
21
+ ## First native server
108
22
 
109
- For detailed contracts intended for coding agents, see the
110
- [REFERENCE.md documentation for agents](REFERENCE.md).
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 own shutdown themselves.
111
26
 
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()`:
27
+ <!-- docs-snippet-path: packages/server/test/server/server-environment.test.ts -->
133
28
 
134
29
  ```ts
135
- import { BoundedContext } from "@spine-event-engine/server";
136
-
137
- const context = BoundedContext.singleTenant("Tasks").persistSystemEvents().build();
138
- await context.close();
139
- ```
140
-
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.
147
-
148
- ## 🧱 Build a bounded context
149
-
150
- Start with an explicit tenant mode. Register entity classes after their Proto
151
- model and generated handler registry are available.
152
-
153
- ```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();
159
- ```
160
-
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.
165
-
166
- ### Keep Stand subscription definitions
167
-
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.
173
-
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.
181
-
182
- ```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();
188
- ```
189
-
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.
219
-
220
- ```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;
241
- ```
242
-
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.
246
-
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.
255
-
256
- ```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 });
276
- }
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();
277
38
  }
278
39
  ```
279
40
 
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";
288
-
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);
293
- ```
294
-
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.
300
-
301
- ```ts
302
- import { Server, type BoundedContext } from "@spine-event-engine/server";
303
-
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}`);
307
- ```
308
-
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.
370
-
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
- ```
407
-
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.
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.
411
45
 
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.
46
+ ## Native lifecycle and routing
420
47
 
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.
48
+ `Server.run()` is the standalone process form: it owns `SIGINT`/`SIGTERM`
49
+ cleanup. `Server.start()` returns a `RunningServer` for an embedding host to
50
+ close. `ManagedServerApplication` is the Node-only complete-replica entrypoint;
51
+ it does not infer process or Delivery-shard counts.
424
52
 
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.
53
+ Routes are declared by generated message schema or interface token. Exact
54
+ schema routes take precedence. Query results from `Stand` are authoritative;
55
+ subscriptions are live, best-effort notifications, so clients re-query after a
56
+ reconnect or a possible gap. Configure a storage provider for durable
57
+ application state and generated repository assembly; in-memory choices are
58
+ for local development and tests.
431
59
 
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)`.
60
+ ## Browser gateway migration
436
61
 
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.
62
+ The server root has no browser options and does not load auth. To host a
63
+ browser-facing Gateway, install the optional auth peer and import the browser
64
+ entry point explicitly:
441
65
 
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.
447
-
448
- ```ts
449
- // docs-snippet-path: packages/server/test/context/bounded-context.test.ts
450
- import type { BoundedContext } from "@spine-event-engine/server";
451
-
452
- declare const context: BoundedContext;
453
- const stand = context.stand();
454
- void stand;
66
+ ```bash
67
+ pnpm add @spine-event-engine/server@snapshot @spine-event-engine/auth@snapshot
455
68
  ```
456
69
 
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
70
  ```ts
483
- import { BoundedContext, UniformAcrossAllShards } from "@spine-event-engine/server";
484
-
485
- const context = BoundedContext.singleTenant("Tasks")
486
- .withDeliveryStrategy(UniformAcrossAllShards.forNumber(3))
487
- .build();
71
+ import { BrowserServer, DurableSubscriptionBindings } from "@spine-event-engine/server/browser";
488
72
  ```
489
73
 
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)
74
+ `BrowserServer.open(...)` composes a browser listener around a caller-managed
75
+ native server; `BrowserServer.run(...)` owns a standalone or combined browser
76
+ host. Supply exact allowed origins, a session or intentional public admission,
77
+ authorization and trusted-context resolvers, finite limits, and backend
78
+ forwarding. Authenticated durable bindings are browser-host infrastructure,
79
+ 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)
80
+ before exposing a backend to a browser.
525
81
 
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:
532
-
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
- ```
82
+ ## Limits
545
83
 
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.
84
+ This package does not create an identity provider, browser session store,
85
+ authorization policy, TLS configuration, or deployment topology. It does not
86
+ guarantee replay, gap repair, exactly-once subscription effects, or
87
+ cluster-complete notification delivery.
553
88
 
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.
89
+ ## Next steps
563
90
 
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.
91
+ - [Detailed coding-agent reference](REFERENCE.md)
92
+ - [Authentication gateway](https://github.com/SpineEventEngine/spine-ts/blob/master/packages/auth/README.md)
93
+ - [Message Board application](https://github.com/SpineEventEngine/spine-ts/blob/master/examples/message-board/app/README.md)