@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.
- package/README.md +63 -539
- package/REFERENCE.md +32 -10
- package/dist/browser/browser-server.d.ts +95 -0
- package/dist/browser/browser-server.d.ts.map +1 -0
- package/dist/{server → browser}/browser-server.js +93 -7
- package/dist/browser/browser-server.js.map +1 -0
- package/dist/browser/durable-subscription-bindings.d.ts.map +1 -0
- package/dist/browser/durable-subscription-bindings.js.map +1 -0
- package/dist/browser/index.d.ts +12 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +24 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/options.d.ts +183 -0
- package/dist/browser/options.d.ts.map +1 -0
- package/dist/browser/options.js +15 -0
- package/dist/browser/options.js.map +1 -0
- package/dist/context/bounded-context.d.ts.map +1 -1
- package/dist/context/bounded-context.js +2 -1
- package/dist/context/bounded-context.js.map +1 -1
- package/dist/context/entity-inbox.js +1 -1
- package/dist/context/entity-inbox.js.map +1 -1
- package/dist/context/local-inbox-handoff.js +1 -1
- package/dist/context/local-inbox-handoff.js.map +1 -1
- package/dist/context/tenant-index.d.ts.map +1 -1
- package/dist/context/tenant-index.js +2 -1
- package/dist/context/tenant-index.js.map +1 -1
- package/dist/delivery/inbox-storage.js +1 -1
- package/dist/delivery/inbox-storage.js.map +1 -1
- package/dist/delivery/sharded-work-registry.d.ts.map +1 -1
- package/dist/delivery/sharded-work-registry.js +2 -1
- package/dist/delivery/sharded-work-registry.js.map +1 -1
- package/dist/entity/entity-storage-descriptor.d.ts +1 -1
- package/dist/entity/entity-storage-descriptor.d.ts.map +1 -1
- package/dist/handler/generated-handler-registry.d.ts +0 -7
- package/dist/handler/generated-handler-registry.d.ts.map +1 -1
- package/dist/handler/generated-handler-registry.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/repository/repository.d.ts.map +1 -1
- package/dist/repository/repository.js +3 -2
- package/dist/repository/repository.js.map +1 -1
- package/dist/server/environment-attachment.d.ts.map +1 -1
- package/dist/server/environment-attachment.js +2 -1
- package/dist/server/environment-attachment.js.map +1 -1
- package/dist/server/environment-delivery-worker.d.ts.map +1 -1
- package/dist/server/environment-delivery-worker.js +2 -1
- package/dist/server/environment-delivery-worker.js.map +1 -1
- package/dist/server/node-coordinator.js +2 -2
- package/dist/server/node-coordinator.js.map +1 -1
- package/dist/server/server.d.ts +9 -177
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +24 -72
- package/dist/server/server.js.map +1 -1
- package/dist/services/spine-services.js +2 -1
- package/dist/services/spine-services.js.map +1 -1
- package/dist/spi/delivery.d.ts +5 -0
- package/dist/spi/delivery.d.ts.map +1 -0
- package/dist/spi/delivery.js +18 -0
- package/dist/spi/delivery.js.map +1 -0
- package/dist/spi/handler-registry.d.ts +8 -0
- package/dist/spi/handler-registry.d.ts.map +1 -0
- package/dist/spi/handler-registry.js +15 -0
- package/dist/spi/handler-registry.js.map +1 -0
- package/dist/stand/stand.d.ts.map +1 -1
- package/dist/stand/stand.js +2 -1
- package/dist/stand/stand.js.map +1 -1
- package/dist/stand/subscription-observer.js +1 -1
- package/dist/stand/subscription-observer.js.map +1 -1
- package/dist/stand/subscription-registry.js +1 -1
- package/dist/stand/subscription-registry.js.map +1 -1
- package/dist/testing/index.d.ts +2 -3
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +2 -3
- package/dist/testing/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +34 -23
- package/dist/handler/build-time-handler-analyzer.d.ts +0 -150
- package/dist/handler/build-time-handler-analyzer.d.ts.map +0 -1
- package/dist/handler/build-time-handler-analyzer.js +0 -1101
- package/dist/handler/build-time-handler-analyzer.js.map +0 -1
- package/dist/handler/generated-registry-writer.d.ts +0 -58
- package/dist/handler/generated-registry-writer.d.ts.map +0 -1
- package/dist/handler/generated-registry-writer.js +0 -458
- package/dist/handler/generated-registry-writer.js.map +0 -1
- package/dist/handler/handler-codegen.d.ts +0 -48
- package/dist/handler/handler-codegen.d.ts.map +0 -1
- package/dist/handler/handler-codegen.js +0 -214
- package/dist/handler/handler-codegen.js.map +0 -1
- package/dist/server/browser-server.d.ts +0 -49
- package/dist/server/browser-server.d.ts.map +0 -1
- package/dist/server/browser-server.js.map +0 -1
- package/dist/server/durable-subscription-bindings.d.ts.map +0 -1
- package/dist/server/durable-subscription-bindings.js.map +0 -1
- /package/dist/{server → browser}/durable-subscription-bindings.d.ts +0 -0
- /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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
7
|
+
> **Experimental prerelease.** Install an exact snapshot while this API evolves:
|
|
8
|
+
> Snapshot releases may remove earlier prerelease import paths.
|
|
8
9
|
|
|
9
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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").
|
|
138
|
-
await context.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
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
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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)
|