@spine-event-engine/server 2.0.0-snapshot.25 → 2.0.0-snapshot.26
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 +49 -9
- package/REFERENCE.md +339 -22
- package/dist/agent/agent-history.d.ts +19 -3
- package/dist/agent/agent-history.d.ts.map +1 -1
- package/dist/agent/agent-history.js +41 -10
- package/dist/agent/agent-history.js.map +1 -1
- package/dist/agent/agent-live-read-runtime.d.ts +12 -1
- package/dist/agent/agent-live-read-runtime.d.ts.map +1 -1
- package/dist/agent/agent-live-read-runtime.js +5 -4
- package/dist/agent/agent-live-read-runtime.js.map +1 -1
- package/dist/browser/browser-server.d.ts +163 -0
- package/dist/browser/browser-server.d.ts.map +1 -1
- package/dist/browser/browser-server.js +579 -188
- package/dist/browser/browser-server.js.map +1 -1
- package/dist/browser/durable-subscription-bindings.d.ts +5 -0
- package/dist/browser/durable-subscription-bindings.d.ts.map +1 -1
- package/dist/browser/durable-subscription-bindings.js +226 -38
- package/dist/browser/durable-subscription-bindings.js.map +1 -1
- package/dist/browser/options.d.ts +5 -0
- package/dist/browser/options.d.ts.map +1 -1
- package/dist/bus/command-bus.d.ts +3 -0
- package/dist/bus/command-bus.d.ts.map +1 -1
- package/dist/bus/command-bus.js +69 -9
- package/dist/bus/command-bus.js.map +1 -1
- package/dist/bus/command-errors.d.ts +20 -4
- package/dist/bus/command-errors.d.ts.map +1 -1
- package/dist/bus/command-errors.js +47 -7
- package/dist/bus/command-errors.js.map +1 -1
- package/dist/bus/event-bus.d.ts +3 -0
- package/dist/bus/event-bus.d.ts.map +1 -1
- package/dist/bus/event-bus.js +130 -8
- package/dist/bus/event-bus.js.map +1 -1
- package/dist/context/bounded-context.d.ts +29 -11
- package/dist/context/bounded-context.d.ts.map +1 -1
- package/dist/context/bounded-context.js +147 -186
- package/dist/context/bounded-context.js.map +1 -1
- package/dist/entity/entity.d.ts +59 -0
- package/dist/entity/entity.d.ts.map +1 -1
- package/dist/entity/entity.js +60 -0
- package/dist/entity/entity.js.map +1 -1
- package/dist/handler/app-query-handler.d.ts +60 -0
- package/dist/handler/app-query-handler.d.ts.map +1 -0
- package/dist/handler/app-query-handler.js +23 -0
- package/dist/handler/app-query-handler.js.map +1 -0
- package/dist/handler/generated-registry-discovery.d.ts +6 -0
- package/dist/handler/generated-registry-discovery.d.ts.map +1 -1
- package/dist/handler/generated-registry-discovery.js +71 -2
- package/dist/handler/generated-registry-discovery.js.map +1 -1
- package/dist/handler/handler-decorators.d.ts +8 -29
- package/dist/handler/handler-decorators.d.ts.map +1 -1
- package/dist/handler/handler-decorators.js +33 -210
- package/dist/handler/handler-decorators.js.map +1 -1
- package/dist/handler/spine-metadata-inventory.d.ts +99 -0
- package/dist/handler/spine-metadata-inventory.d.ts.map +1 -0
- package/dist/handler/spine-metadata-inventory.js +162 -0
- package/dist/handler/spine-metadata-inventory.js.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/repository/repository.d.ts +44 -1
- package/dist/repository/repository.d.ts.map +1 -1
- package/dist/repository/repository.js +368 -88
- package/dist/repository/repository.js.map +1 -1
- package/dist/server/managed-server-application.d.ts +12 -11
- package/dist/server/managed-server-application.d.ts.map +1 -1
- package/dist/server/managed-server-application.js +41 -25
- package/dist/server/managed-server-application.js.map +1 -1
- package/dist/server/node-coordinator.d.ts +18 -5
- package/dist/server/node-coordinator.d.ts.map +1 -1
- package/dist/server/node-coordinator.js +554 -84
- package/dist/server/node-coordinator.js.map +1 -1
- package/dist/server/reserved-spine-rpc-paths.json +1 -0
- package/dist/server/server-environment.d.ts +17 -0
- package/dist/server/server-environment.d.ts.map +1 -1
- package/dist/server/server-environment.js +77 -16
- package/dist/server/server-environment.js.map +1 -1
- package/dist/server/server.d.ts +45 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +111 -3
- package/dist/server/server.js.map +1 -1
- package/dist/services/app-query-execution.d.ts +53 -0
- package/dist/services/app-query-execution.d.ts.map +1 -0
- package/dist/services/app-query-execution.js +408 -0
- package/dist/services/app-query-execution.js.map +1 -0
- package/dist/services/query-reader.d.ts +32 -7
- package/dist/services/query-reader.d.ts.map +1 -1
- package/dist/services/query-reader.js +22 -2
- package/dist/services/query-reader.js.map +1 -1
- package/dist/services/read-request-status.d.ts +65 -0
- package/dist/services/read-request-status.d.ts.map +1 -0
- package/dist/services/read-request-status.js +151 -0
- package/dist/services/read-request-status.js.map +1 -0
- package/dist/services/spine-services.d.ts +36 -7
- package/dist/services/spine-services.d.ts.map +1 -1
- package/dist/services/spine-services.js +900 -168
- package/dist/services/spine-services.js.map +1 -1
- package/dist/services/trusted-reader-ingress.d.ts +92 -0
- package/dist/services/trusted-reader-ingress.d.ts.map +1 -0
- package/dist/services/trusted-reader-ingress.js +89 -0
- package/dist/services/trusted-reader-ingress.js.map +1 -0
- package/dist/spi/handler-registry.d.ts +2 -0
- package/dist/spi/handler-registry.d.ts.map +1 -1
- package/dist/spi/handler-registry.js +1 -1
- package/dist/spi/handler-registry.js.map +1 -1
- package/dist/stand/read-side-filter-engine.d.ts +46 -0
- package/dist/stand/read-side-filter-engine.d.ts.map +1 -0
- package/dist/stand/read-side-filter-engine.js +356 -0
- package/dist/stand/read-side-filter-engine.js.map +1 -0
- package/dist/stand/read-side-filter.d.ts +247 -0
- package/dist/stand/read-side-filter.d.ts.map +1 -0
- package/dist/stand/read-side-filter.js +39 -0
- package/dist/stand/read-side-filter.js.map +1 -0
- package/dist/stand/read-side-message.d.ts +66 -0
- package/dist/stand/read-side-message.d.ts.map +1 -0
- package/dist/stand/read-side-message.js +167 -0
- package/dist/stand/read-side-message.js.map +1 -0
- package/dist/stand/stand-direct-delivery.d.ts +161 -0
- package/dist/stand/stand-direct-delivery.d.ts.map +1 -0
- package/dist/stand/stand-direct-delivery.js +210 -0
- package/dist/stand/stand-direct-delivery.js.map +1 -0
- package/dist/stand/stand.d.ts +80 -42
- package/dist/stand/stand.d.ts.map +1 -1
- package/dist/stand/stand.js +410 -54
- package/dist/stand/stand.js.map +1 -1
- package/dist/stand/subscription-observer.d.ts.map +1 -1
- package/dist/stand/subscription-observer.js +103 -0
- package/dist/stand/subscription-observer.js.map +1 -1
- package/dist/stand/subscription-read-filters.d.ts +40 -0
- package/dist/stand/subscription-read-filters.d.ts.map +1 -0
- package/dist/stand/subscription-read-filters.js +91 -0
- package/dist/stand/subscription-read-filters.js.map +1 -0
- package/dist/stand/subscription-reader-binding.d.ts +20 -0
- package/dist/stand/subscription-reader-binding.d.ts.map +1 -0
- package/dist/stand/subscription-reader-binding.js +56 -0
- package/dist/stand/subscription-reader-binding.js.map +1 -0
- package/dist/stand/subscription-records.d.ts +30 -3
- package/dist/stand/subscription-records.d.ts.map +1 -1
- package/dist/stand/subscription-records.js +61 -16
- package/dist/stand/subscription-records.js.map +1 -1
- package/dist/stand/subscription-registry.d.ts +49 -14
- package/dist/stand/subscription-registry.d.ts.map +1 -1
- package/dist/stand/subscription-registry.js +150 -31
- package/dist/stand/subscription-registry.js.map +1 -1
- package/dist/stand/subscription-runtime.d.ts +7 -2
- package/dist/stand/subscription-runtime.d.ts.map +1 -1
- package/dist/stand/subscription-runtime.js +131 -25
- package/dist/stand/subscription-runtime.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +14 -14
|
@@ -34,6 +34,13 @@ const reservedPathSet = new Set(reservedSpineRpcPaths);
|
|
|
34
34
|
* Hosts browser-facing Spine gateway routes independently from native server assembly.
|
|
35
35
|
*/
|
|
36
36
|
export const browserServerImplementation = Object.freeze({
|
|
37
|
+
/**
|
|
38
|
+
* Starts a browser listener against the supplied native backend.
|
|
39
|
+
*
|
|
40
|
+
* @param native The native backend to expose through this browser host.
|
|
41
|
+
* @param input Browser configuration to validate before opening the listener.
|
|
42
|
+
* @returns The listening browser endpoint with coordinated native-backend cleanup.
|
|
43
|
+
*/
|
|
37
44
|
async open(native, input) {
|
|
38
45
|
const running = BrowserServerValues.running(native);
|
|
39
46
|
if (running !== undefined && !BrowserServerValues.isLoopback(running.host))
|
|
@@ -46,195 +53,15 @@ export const browserServerImplementation = Object.freeze({
|
|
|
46
53
|
await BrowserServerValues.rollback(error, running === undefined ? [] : [() => running.close()]);
|
|
47
54
|
throw error;
|
|
48
55
|
}
|
|
49
|
-
|
|
50
|
-
const authRoutes = BrowserServer.authRoutes(options.authRoutes);
|
|
51
|
-
const activeAuth = new Set();
|
|
52
|
-
const maxActiveAuthRequests = options.maxActiveAuthRequests ?? 64;
|
|
53
|
-
if (!Number.isSafeInteger(maxActiveAuthRequests) || maxActiveAuthRequests < 1)
|
|
54
|
-
throw new Error("Browser maxActiveAuthRequests must be a positive safe integer.");
|
|
55
|
-
let draining = false;
|
|
56
|
-
const backendBaseUrls = native === undefined
|
|
57
|
-
? options.backend === undefined
|
|
58
|
-
? []
|
|
59
|
-
: BrowserServer.backendUrls(BrowserServerValues.backendUrlsFor(options.backend))
|
|
60
|
-
: Array.isArray(native)
|
|
61
|
-
? BrowserServer.backendUrls(native)
|
|
62
|
-
: [
|
|
63
|
-
typeof native === "string"
|
|
64
|
-
? native
|
|
65
|
-
: BrowserServerValues.requiredRunning(running).baseUrl,
|
|
66
|
-
];
|
|
67
|
-
const fixedNodes = backendBaseUrls.map((endpoint, index) => new ApplicationNode({ id: `fixed/${index.toString()}`, endpoint }));
|
|
68
|
-
let dynamic;
|
|
69
|
-
let stopDiscovery;
|
|
70
|
-
const forwarder = (dynamic = new DynamicUnaryForwarder({
|
|
71
|
-
create: async (node) => {
|
|
72
|
-
const manager = options.dynamicManagerFactory?.(node) ??
|
|
73
|
-
new Http2SessionManager(node.endpoint, undefined, node.tlsServerName === undefined ? undefined : { servername: node.tlsServerName });
|
|
74
|
-
const client = new NativeSubscriptionCreator(createGrpcTransport({
|
|
75
|
-
...BrowserServer.dynamicTransportOptions(node),
|
|
76
|
-
sessionManager: manager,
|
|
77
|
-
}));
|
|
78
|
-
return {
|
|
79
|
-
forward: client.forward.bind(client),
|
|
80
|
-
subscribe: client.subscribe.bind(client),
|
|
81
|
-
activate: client.activate.bind(client),
|
|
82
|
-
cancel: client.cancel.bind(client),
|
|
83
|
-
dispose: client.dispose.bind(client),
|
|
84
|
-
close: async () => {
|
|
85
|
-
manager.abort();
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
},
|
|
89
|
-
}));
|
|
90
|
-
const creator = new DynamicSubscriptionCreator(dynamic);
|
|
91
|
-
const bindings = options.bindings ??
|
|
92
|
-
new InMemorySubscriptionBindings({
|
|
93
|
-
nextId: () => globalThis.crypto.randomUUID(),
|
|
94
|
-
dispose: (definition, signal) => creator.cancel({ wire: definition }, signal),
|
|
95
|
-
});
|
|
96
|
-
const requests = BrowserServer.requests(options);
|
|
97
|
-
const admission = options.publicAccess === true
|
|
98
|
-
? { publicAccess: true }
|
|
99
|
-
: { sessions: options.sessions };
|
|
100
|
-
const unary = new UnaryGateway({
|
|
101
|
-
...(options.registry === undefined ? {} : { registry: options.registry }),
|
|
102
|
-
maxRequestBytes: 1_048_576,
|
|
103
|
-
...admission,
|
|
104
|
-
authorize: options.authorize,
|
|
105
|
-
contexts: options.contexts,
|
|
106
|
-
clock: options.clock,
|
|
107
|
-
forward: forwarder.forward.bind(forwarder),
|
|
108
|
-
});
|
|
109
|
-
const subscriptions = new SubscriptionGateway({
|
|
110
|
-
bindings,
|
|
111
|
-
...admission,
|
|
112
|
-
authorize: options.authorize,
|
|
113
|
-
contexts: options.contexts,
|
|
114
|
-
clock: options.clock,
|
|
115
|
-
creator,
|
|
116
|
-
});
|
|
117
|
-
try {
|
|
118
|
-
if (options.discovery !== undefined)
|
|
119
|
-
stopDiscovery = await BrowserServerValues.watch(options.discovery, dynamic);
|
|
120
|
-
else
|
|
121
|
-
await dynamic.reconcile(fixedNodes);
|
|
122
|
-
if (isDurableSubscriptionBindings(bindings))
|
|
123
|
-
attachDurableSubscriptionCleanup(bindings, (definition, signal) => creator.cancel({ wire: definition }, signal));
|
|
124
|
-
const durableBindings = bindings;
|
|
125
|
-
if (durableBindings.recoverActive !== undefined) {
|
|
126
|
-
const now = options.clock.now();
|
|
127
|
-
const nowMs = Number(now.seconds.toString()) * 1_000 +
|
|
128
|
-
Math.floor(Number(now.nanos.toString()) / 1_000_000);
|
|
129
|
-
if (Number.isSafeInteger(nowMs))
|
|
130
|
-
await durableBindings.recoverActive({
|
|
131
|
-
nowMs,
|
|
132
|
-
onDefinition: async (definition, whenExpires) => {
|
|
133
|
-
await creator.rehydrate(definition);
|
|
134
|
-
subscriptions.scheduleExpiry(whenExpires);
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
catch (error) {
|
|
140
|
-
const failures = [error];
|
|
141
|
-
for (const cleanup of [
|
|
142
|
-
() => subscriptions.close(),
|
|
143
|
-
() => stopDiscovery?.(),
|
|
144
|
-
() => dynamic.close(),
|
|
145
|
-
() => running?.close(),
|
|
146
|
-
])
|
|
147
|
-
try {
|
|
148
|
-
await cleanup();
|
|
149
|
-
}
|
|
150
|
-
catch (cleanupError) {
|
|
151
|
-
failures.push(cleanupError);
|
|
152
|
-
}
|
|
153
|
-
if (failures.length === 1)
|
|
154
|
-
throw error;
|
|
155
|
-
throw new AggregateError(failures, "Browser subscription recovery startup rollback failed.");
|
|
156
|
-
}
|
|
157
|
-
const services = createNativeGatewayServices({ unary, subscriptions, requests });
|
|
158
|
-
const handler = connectNodeAdapter({
|
|
159
|
-
routes: (router) => {
|
|
160
|
-
router.service(AuthenticationService, services.authentication);
|
|
161
|
-
router.service(CommandService, services.command);
|
|
162
|
-
router.service(QueryService, services.query);
|
|
163
|
-
router.service(SubscriptionService, services.subscription);
|
|
164
|
-
},
|
|
165
|
-
readMaxBytes: options.readMaxBytes,
|
|
166
|
-
writeMaxBytes: options.writeMaxBytes,
|
|
167
|
-
});
|
|
168
|
-
const server = http.createServer((request, response) => {
|
|
169
|
-
if (draining) {
|
|
170
|
-
response.statusCode = 503;
|
|
171
|
-
response.end();
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
const path = request.url ?? "";
|
|
175
|
-
const auth = authRoutes.get(path);
|
|
176
|
-
if (auth !== undefined && request.method === auth.method) {
|
|
177
|
-
if (activeAuth.size >= maxActiveAuthRequests) {
|
|
178
|
-
response.statusCode = 503;
|
|
179
|
-
response.end();
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
void BrowserServer.dispatchAuth(request, response, auth, activeAuth, options.writeMaxBytes);
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
if (auth !== undefined) {
|
|
186
|
-
const origin = request.headers.origin;
|
|
187
|
-
if (request.method === "OPTIONS" && origin !== undefined && auth.origins.includes(origin)) {
|
|
188
|
-
response.setHeader("access-control-allow-origin", origin);
|
|
189
|
-
response.setHeader("access-control-allow-methods", `${auth.method},OPTIONS`);
|
|
190
|
-
response.statusCode = 204;
|
|
191
|
-
}
|
|
192
|
-
else
|
|
193
|
-
response.statusCode = request.method === "OPTIONS" ? 403 : 405;
|
|
194
|
-
response.end();
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
const origin = request.headers.origin;
|
|
198
|
-
if (origin === undefined || !origins.has(origin)) {
|
|
199
|
-
response.statusCode = 403;
|
|
200
|
-
response.end();
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
response.setHeader("access-control-allow-origin", origin);
|
|
204
|
-
response.setHeader("access-control-allow-credentials", "true");
|
|
205
|
-
response.setHeader("access-control-expose-headers", "grpc-status,grpc-message");
|
|
206
|
-
response.setHeader("vary", "Origin");
|
|
207
|
-
if (request.method === "OPTIONS") {
|
|
208
|
-
response.setHeader("access-control-allow-methods", "POST,OPTIONS");
|
|
209
|
-
response.setHeader("access-control-allow-headers", "authorization,content-type,connect-protocol-version,x-csrf-token");
|
|
210
|
-
response.setHeader("access-control-max-age", "600");
|
|
211
|
-
response.statusCode = 204;
|
|
212
|
-
response.end();
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
handler(request, response);
|
|
216
|
-
});
|
|
217
|
-
let address;
|
|
218
|
-
try {
|
|
219
|
-
address = await BrowserServer.listen(server, options.host, options.port);
|
|
220
|
-
}
|
|
221
|
-
catch (error) {
|
|
222
|
-
const cleanup = await Promise.allSettled([
|
|
223
|
-
subscriptions.close(),
|
|
224
|
-
stopDiscovery?.(),
|
|
225
|
-
dynamic.close(),
|
|
226
|
-
BrowserServer.closeListener(server),
|
|
227
|
-
running?.close(),
|
|
228
|
-
]);
|
|
229
|
-
const failures = cleanup.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
230
|
-
if (failures.length > 0)
|
|
231
|
-
throw new AggregateError([error, ...failures], "Server browser startup rollback failed.");
|
|
232
|
-
throw error;
|
|
233
|
-
}
|
|
234
|
-
return new RunningBrowserServer(server, running, subscriptions, address, activeAuth, () => {
|
|
235
|
-
draining = true;
|
|
236
|
-
}, stopDiscovery, dynamic);
|
|
56
|
+
return BrowserServerValues.openWithOptions(native, running, options);
|
|
237
57
|
},
|
|
58
|
+
/**
|
|
59
|
+
* Starts the browser host and registers it for process-level shutdown.
|
|
60
|
+
*
|
|
61
|
+
* @param nativeOrOptions The local Server to start, or options for a standalone browser gateway.
|
|
62
|
+
* @param suppliedOptions Browser configuration when a local Server is supplied separately.
|
|
63
|
+
* @returns The running host registered with the process coordinator.
|
|
64
|
+
*/
|
|
238
65
|
async run(nativeOrOptions, suppliedOptions) {
|
|
239
66
|
const options = suppliedOptions ?? nativeOrOptions;
|
|
240
67
|
if (nativeOrOptions instanceof Server) {
|
|
@@ -250,6 +77,13 @@ export const browserServerImplementation = Object.freeze({
|
|
|
250
77
|
: await BrowserServer.open(native, options);
|
|
251
78
|
return ProcessServerCoordinator.add(running, native === undefined ? undefined : ServerEnvironment.instance(), undefined, () => undefined);
|
|
252
79
|
},
|
|
80
|
+
/**
|
|
81
|
+
* Applies listener defaults and checks browser admission configuration before startup.
|
|
82
|
+
*
|
|
83
|
+
* @param mode Whether a native server is started locally or supplied as a separate backend.
|
|
84
|
+
* @param input Browser configuration to validate before opening the listener.
|
|
85
|
+
* @returns Normalized listener options after all preflight checks pass.
|
|
86
|
+
*/
|
|
253
87
|
preflight(mode, input) {
|
|
254
88
|
const options = {
|
|
255
89
|
...input,
|
|
@@ -277,6 +111,12 @@ export const browserServerImplementation = Object.freeze({
|
|
|
277
111
|
throw new Error("Browser maxActiveAuthRequests must be a positive safe integer.");
|
|
278
112
|
return options;
|
|
279
113
|
},
|
|
114
|
+
/**
|
|
115
|
+
* Builds credential extraction and transport-fact adapters for browser requests.
|
|
116
|
+
*
|
|
117
|
+
* @param options Browser listener, admission, and backend configuration.
|
|
118
|
+
* @returns Adapters that the gateway uses to admit each incoming request.
|
|
119
|
+
*/
|
|
280
120
|
requests(options) {
|
|
281
121
|
return {
|
|
282
122
|
credential: (context) => {
|
|
@@ -301,6 +141,12 @@ export const browserServerImplementation = Object.freeze({
|
|
|
301
141
|
},
|
|
302
142
|
};
|
|
303
143
|
},
|
|
144
|
+
/**
|
|
145
|
+
* Validates a nonempty set of unique canonical HTTP(S) origins.
|
|
146
|
+
*
|
|
147
|
+
* @param values Browser origins to validate and deduplicate.
|
|
148
|
+
* @returns The validated origins for exact admission checks.
|
|
149
|
+
*/
|
|
304
150
|
origins(values) {
|
|
305
151
|
const origins = new Set(values.map((value) => {
|
|
306
152
|
let origin;
|
|
@@ -323,6 +169,12 @@ export const browserServerImplementation = Object.freeze({
|
|
|
323
169
|
throw new Error("Server browser origins must be unique and non-empty.");
|
|
324
170
|
return origins;
|
|
325
171
|
},
|
|
172
|
+
/**
|
|
173
|
+
* Rejects backend URLs containing paths, credentials, queries, or fragments.
|
|
174
|
+
*
|
|
175
|
+
* @param value The backend origin to validate.
|
|
176
|
+
* @returns The original canonical backend origin.
|
|
177
|
+
*/
|
|
326
178
|
backendUrl(value) {
|
|
327
179
|
let backend;
|
|
328
180
|
try {
|
|
@@ -342,6 +194,12 @@ export const browserServerImplementation = Object.freeze({
|
|
|
342
194
|
}
|
|
343
195
|
return value;
|
|
344
196
|
},
|
|
197
|
+
/**
|
|
198
|
+
* Validates a nonempty list of distinct native backend origins.
|
|
199
|
+
*
|
|
200
|
+
* @param values Native backend origins in routing order.
|
|
201
|
+
* @returns The validated backend origins in their configured order.
|
|
202
|
+
*/
|
|
345
203
|
backendUrls(values) {
|
|
346
204
|
if (values.length < 1)
|
|
347
205
|
throw new Error("Server browser backends must contain at least one origin.");
|
|
@@ -350,6 +208,12 @@ export const browserServerImplementation = Object.freeze({
|
|
|
350
208
|
throw new Error("Server browser backends must be unique canonical HTTP(S) origins.");
|
|
351
209
|
return urls;
|
|
352
210
|
},
|
|
211
|
+
/**
|
|
212
|
+
* Selects the node endpoint and its configured TLS server name.
|
|
213
|
+
*
|
|
214
|
+
* @param node The application node whose native transport is being configured.
|
|
215
|
+
* @returns Transport settings preserving the node's TLS server-name override.
|
|
216
|
+
*/
|
|
353
217
|
dynamicTransportOptions(node) {
|
|
354
218
|
return {
|
|
355
219
|
baseUrl: node.endpoint,
|
|
@@ -358,6 +222,12 @@ export const browserServerImplementation = Object.freeze({
|
|
|
358
222
|
: { nodeOptions: { servername: node.tlsServerName } }),
|
|
359
223
|
};
|
|
360
224
|
},
|
|
225
|
+
/**
|
|
226
|
+
* Checks admission ports and production subscription-persistence requirements.
|
|
227
|
+
*
|
|
228
|
+
* @param options Browser listener, admission, and backend configuration.
|
|
229
|
+
* @param production Whether production persistence requirements must be enforced.
|
|
230
|
+
*/
|
|
361
231
|
requireDurableBindings(options, production) {
|
|
362
232
|
const supplied = options;
|
|
363
233
|
const standalone = options.backend !== undefined || options.discovery !== undefined;
|
|
@@ -396,6 +266,12 @@ export const browserServerImplementation = Object.freeze({
|
|
|
396
266
|
!bindings.namespace.trim()))
|
|
397
267
|
throw new Error("Production standalone browser server requires named durable subscription bindings.");
|
|
398
268
|
},
|
|
269
|
+
/**
|
|
270
|
+
* Validates authentication routes and rejects duplicate or reserved RPC paths.
|
|
271
|
+
*
|
|
272
|
+
* @param routes Optional HTTP authentication endpoints to validate and index.
|
|
273
|
+
* @returns Validated routes indexed by their exact HTTP path.
|
|
274
|
+
*/
|
|
399
275
|
authRoutes(routes) {
|
|
400
276
|
const result = new Map();
|
|
401
277
|
for (const route of routes ?? []) {
|
|
@@ -421,6 +297,16 @@ export const browserServerImplementation = Object.freeze({
|
|
|
421
297
|
}
|
|
422
298
|
return result;
|
|
423
299
|
},
|
|
300
|
+
/**
|
|
301
|
+
* Executes an authentication route within its configured origin, size, and time bounds.
|
|
302
|
+
*
|
|
303
|
+
* @param request The incoming HTTP request.
|
|
304
|
+
* @param response The HTTP response to complete with the route outcome.
|
|
305
|
+
* @param route The matched authentication endpoint and its admission constraints.
|
|
306
|
+
* @param active Controllers tracked so shutdown can abort active authentication requests.
|
|
307
|
+
* @param writeMaxBytes The existing transport bound for the authentication response body.
|
|
308
|
+
* @returns Completion after the HTTP response and request cleanup settle.
|
|
309
|
+
*/
|
|
424
310
|
async dispatchAuth(request, response, route, active, writeMaxBytes) {
|
|
425
311
|
const origin = request.headers.origin;
|
|
426
312
|
if ((origin === undefined && route.allowMissingOrigin !== true) ||
|
|
@@ -528,6 +414,14 @@ export const browserServerImplementation = Object.freeze({
|
|
|
528
414
|
active.delete(controller);
|
|
529
415
|
}
|
|
530
416
|
},
|
|
417
|
+
/**
|
|
418
|
+
* Binds the HTTP listener and propagates startup errors.
|
|
419
|
+
*
|
|
420
|
+
* @param server The HTTP listener being started or stopped.
|
|
421
|
+
* @param host The interface address on which to bind the listener.
|
|
422
|
+
* @param port The requested listener port; zero requests an ephemeral port.
|
|
423
|
+
* @returns The actual bound address, including an allocated ephemeral port.
|
|
424
|
+
*/
|
|
531
425
|
listen(server, host, port) {
|
|
532
426
|
return new Promise((resolve, reject) => {
|
|
533
427
|
const onError = (error) => {
|
|
@@ -540,6 +434,12 @@ export const browserServerImplementation = Object.freeze({
|
|
|
540
434
|
});
|
|
541
435
|
});
|
|
542
436
|
},
|
|
437
|
+
/**
|
|
438
|
+
* Stops accepting connections and drains the HTTP listener.
|
|
439
|
+
*
|
|
440
|
+
* @param server The HTTP listener being started or stopped.
|
|
441
|
+
* @returns Completion after the listener closes; rejects on a close error.
|
|
442
|
+
*/
|
|
543
443
|
closeListener(server) {
|
|
544
444
|
return new Promise((resolve, reject) => {
|
|
545
445
|
if (!server.listening) {
|
|
@@ -570,22 +470,457 @@ export const browserServerTestAccess = Object.freeze({
|
|
|
570
470
|
});
|
|
571
471
|
const BrowserServer = browserServerImplementation;
|
|
572
472
|
const BrowserServerValues = Object.freeze({
|
|
473
|
+
/**
|
|
474
|
+
* Builds the admitted browser listener from normalized options.
|
|
475
|
+
*
|
|
476
|
+
* @param native The native backend to expose through this browser host.
|
|
477
|
+
* @param running The local native listener to include in coordinated cleanup, when present.
|
|
478
|
+
* @param options Browser listener, admission, and backend configuration.
|
|
479
|
+
* @returns The listening host after membership and durable recovery are ready.
|
|
480
|
+
*/
|
|
481
|
+
async openWithOptions(native, running, options) {
|
|
482
|
+
const activeAuth = new Set();
|
|
483
|
+
let draining = false;
|
|
484
|
+
const { dynamic, requests, unary, subscriptions, stopDiscovery } = await BrowserServerValues.readyGraph(native, running, options);
|
|
485
|
+
const server = BrowserServerValues.admittedListener(options, activeAuth, () => draining, unary, subscriptions, requests);
|
|
486
|
+
return BrowserServerValues.finishBrowser({
|
|
487
|
+
options,
|
|
488
|
+
running,
|
|
489
|
+
subscriptions,
|
|
490
|
+
stopDiscovery,
|
|
491
|
+
dynamic,
|
|
492
|
+
activeAuth,
|
|
493
|
+
server,
|
|
494
|
+
onDrain: () => {
|
|
495
|
+
draining = true;
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
},
|
|
499
|
+
/**
|
|
500
|
+
* Starts discovery and durable recovery after assembling the gateway graph.
|
|
501
|
+
*
|
|
502
|
+
* @param native The native backend to expose through this browser host.
|
|
503
|
+
* @param running The local native listener to include in coordinated cleanup, when present.
|
|
504
|
+
* @param options Browser listener, admission, and backend configuration.
|
|
505
|
+
* @returns Gateway collaborators and the discovery cleanup callback, when discovery is configured.
|
|
506
|
+
*/
|
|
507
|
+
async readyGraph(native, running, options) {
|
|
508
|
+
const graph = BrowserServerValues.prepareBrowser(native, running, options);
|
|
509
|
+
const stopDiscovery = await BrowserServerValues.startMembership(options, running, graph.fixedNodes, graph.dynamic, graph.creator, graph.bindings, graph.subscriptions);
|
|
510
|
+
return { ...graph, stopDiscovery };
|
|
511
|
+
},
|
|
512
|
+
/**
|
|
513
|
+
* Applies configured browser origin and auth-route policy to the listener.
|
|
514
|
+
*
|
|
515
|
+
* @param options Browser listener, admission, and backend configuration.
|
|
516
|
+
* @param activeAuth Controllers tracked for cancellation during browser-host shutdown.
|
|
517
|
+
* @param draining Reports whether shutdown has stopped admission of new requests.
|
|
518
|
+
* @param unary The gateway that admits and forwards unary Spine requests.
|
|
519
|
+
* @param subscriptions The gateway managing admitted browser subscriptions.
|
|
520
|
+
* @param requests Adapters for extracting credentials and transport facts from HTTP headers.
|
|
521
|
+
* @returns An HTTP server configured with admission routes but not yet listening.
|
|
522
|
+
*/
|
|
523
|
+
admittedListener(options, activeAuth, draining, unary, subscriptions, requests) {
|
|
524
|
+
const maxActiveAuthRequests = options.maxActiveAuthRequests ?? 64;
|
|
525
|
+
if (!Number.isSafeInteger(maxActiveAuthRequests) || maxActiveAuthRequests < 1)
|
|
526
|
+
throw new Error("Browser maxActiveAuthRequests must be a positive safe integer.");
|
|
527
|
+
return BrowserServerValues.createListener({
|
|
528
|
+
options,
|
|
529
|
+
origins: BrowserServer.origins(options.origins),
|
|
530
|
+
authRoutes: BrowserServer.authRoutes(options.authRoutes),
|
|
531
|
+
activeAuth,
|
|
532
|
+
maxActiveAuthRequests,
|
|
533
|
+
draining,
|
|
534
|
+
unary,
|
|
535
|
+
subscriptions,
|
|
536
|
+
requests,
|
|
537
|
+
});
|
|
538
|
+
},
|
|
539
|
+
/**
|
|
540
|
+
* Builds one native backend and the admission collaborators before discovery.
|
|
541
|
+
*
|
|
542
|
+
* @param native The native backend to expose through this browser host.
|
|
543
|
+
* @param running The local native listener to include in coordinated cleanup, when present.
|
|
544
|
+
* @param options Browser listener, admission, and backend configuration.
|
|
545
|
+
* @returns The backend, binding, and admission collaborators needed for startup.
|
|
546
|
+
*/
|
|
547
|
+
prepareBrowser(native, running, options) {
|
|
548
|
+
const fixedNodes = BrowserServerValues.fixedNodes(native, running, options);
|
|
549
|
+
const dynamic = BrowserServerValues.dynamic(options);
|
|
550
|
+
const creator = new DynamicSubscriptionCreator(dynamic);
|
|
551
|
+
const bindings = BrowserServerValues.bindings(options, creator);
|
|
552
|
+
const { requests, unary, subscriptions } = BrowserServerValues.admission(options, dynamic, creator, bindings);
|
|
553
|
+
return { fixedNodes, dynamic, creator, bindings, requests, unary, subscriptions };
|
|
554
|
+
},
|
|
555
|
+
/**
|
|
556
|
+
* Returns durable bindings or constructs the local in-memory admission port.
|
|
557
|
+
*
|
|
558
|
+
* @param options Browser listener, admission, and backend configuration.
|
|
559
|
+
* @param creator The native subscription adapter used for creation, recovery, and cancellation.
|
|
560
|
+
* @returns The supplied binding store or a new process-local store with native cancellation.
|
|
561
|
+
*/
|
|
562
|
+
bindings(options, creator) {
|
|
563
|
+
return (options.bindings ??
|
|
564
|
+
new InMemorySubscriptionBindings({
|
|
565
|
+
nextId: () => globalThis.crypto.randomUUID(),
|
|
566
|
+
dispose: (definition, signal) => creator.cancel({ wire: definition }, signal),
|
|
567
|
+
}));
|
|
568
|
+
},
|
|
569
|
+
/**
|
|
570
|
+
* Publishes one assembled listener after startup rollback is established.
|
|
571
|
+
*
|
|
572
|
+
* @param input Started browser resources and their coordinated shutdown callbacks.
|
|
573
|
+
* @returns The published browser endpoint with coordinated resource cleanup.
|
|
574
|
+
*/
|
|
575
|
+
async finishBrowser(input) {
|
|
576
|
+
const address = await BrowserServerValues.listenStarted(input);
|
|
577
|
+
return new RunningBrowserServer(input.server, input.running, input.subscriptions, address, input.activeAuth, input.onDrain, input.stopDiscovery, input.dynamic);
|
|
578
|
+
},
|
|
579
|
+
/**
|
|
580
|
+
* Rolls back all started browser resources when listener bind fails.
|
|
581
|
+
*
|
|
582
|
+
* @param input Started browser resources and their coordinated shutdown callbacks.
|
|
583
|
+
* @returns The bound listener address; startup failures trigger resource cleanup.
|
|
584
|
+
*/
|
|
585
|
+
async listenStarted(input) {
|
|
586
|
+
try {
|
|
587
|
+
return await BrowserServer.listen(input.server, input.options.host, input.options.port);
|
|
588
|
+
}
|
|
589
|
+
catch (error) {
|
|
590
|
+
const cleanup = await Promise.allSettled([
|
|
591
|
+
input.subscriptions.close(),
|
|
592
|
+
input.stopDiscovery?.(),
|
|
593
|
+
input.dynamic.close(),
|
|
594
|
+
BrowserServer.closeListener(input.server),
|
|
595
|
+
input.running?.close(),
|
|
596
|
+
]);
|
|
597
|
+
const failures = cleanup.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
598
|
+
if (failures.length > 0)
|
|
599
|
+
throw new AggregateError([error, ...failures], "Server browser startup rollback failed.");
|
|
600
|
+
throw error;
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
/**
|
|
604
|
+
* Resolves fixed native backends without changing dynamic discovery behavior.
|
|
605
|
+
*
|
|
606
|
+
* @param native The native backend to expose through this browser host.
|
|
607
|
+
* @param running The local native listener to include in coordinated cleanup, when present.
|
|
608
|
+
* @param options Browser listener, admission, and backend configuration.
|
|
609
|
+
* @returns Configured fixed endpoints represented as application nodes.
|
|
610
|
+
*/
|
|
611
|
+
fixedNodes(native, running, options) {
|
|
612
|
+
const urls = native === undefined
|
|
613
|
+
? options.backend === undefined
|
|
614
|
+
? []
|
|
615
|
+
: BrowserServer.backendUrls(BrowserServerValues.backendUrlsFor(options.backend))
|
|
616
|
+
: Array.isArray(native)
|
|
617
|
+
? BrowserServer.backendUrls(native)
|
|
618
|
+
: [
|
|
619
|
+
typeof native === "string"
|
|
620
|
+
? native
|
|
621
|
+
: BrowserServerValues.requiredRunning(running).baseUrl,
|
|
622
|
+
];
|
|
623
|
+
return urls.map((endpoint, index) => new ApplicationNode({ id: `fixed/${index.toString()}`, endpoint }));
|
|
624
|
+
},
|
|
625
|
+
/**
|
|
626
|
+
* Creates the dynamically reconciled native transport adapter.
|
|
627
|
+
*
|
|
628
|
+
* @param options Browser listener, admission, and backend configuration.
|
|
629
|
+
* @returns A forwarder that creates native clients as node membership changes.
|
|
630
|
+
*/
|
|
631
|
+
dynamic(options) {
|
|
632
|
+
return new DynamicUnaryForwarder({
|
|
633
|
+
create: (node) => BrowserServerValues.nativeClient(node, options),
|
|
634
|
+
});
|
|
635
|
+
},
|
|
636
|
+
/**
|
|
637
|
+
* Opens a native client with the configured reader proof producer.
|
|
638
|
+
*
|
|
639
|
+
* @param node The application node whose native transport is being configured.
|
|
640
|
+
* @param options Browser listener, admission, and backend configuration.
|
|
641
|
+
* @returns Native RPC operations and a callback that aborts their HTTP/2 session manager.
|
|
642
|
+
*/
|
|
643
|
+
async nativeClient(node, options) {
|
|
644
|
+
const manager = options.dynamicManagerFactory?.(node) ??
|
|
645
|
+
new Http2SessionManager(node.endpoint, undefined, node.tlsServerName === undefined ? undefined : { servername: node.tlsServerName });
|
|
646
|
+
const client = new NativeSubscriptionCreator(createGrpcTransport({
|
|
647
|
+
...BrowserServer.dynamicTransportOptions(node),
|
|
648
|
+
sessionManager: manager,
|
|
649
|
+
}), options.forwardingProof === undefined
|
|
650
|
+
? undefined
|
|
651
|
+
: {
|
|
652
|
+
issuer: options.forwardingProof,
|
|
653
|
+
maxBytes: options.readMaxBytes,
|
|
654
|
+
});
|
|
655
|
+
return {
|
|
656
|
+
forward: client.forward.bind(client),
|
|
657
|
+
subscribe: client.subscribe.bind(client),
|
|
658
|
+
activate: client.activate.bind(client),
|
|
659
|
+
cancel: client.cancel.bind(client),
|
|
660
|
+
dispose: client.dispose.bind(client),
|
|
661
|
+
close: async () => {
|
|
662
|
+
manager.abort();
|
|
663
|
+
},
|
|
664
|
+
};
|
|
665
|
+
},
|
|
666
|
+
/**
|
|
667
|
+
* Creates unary and subscription admission around one reader-aware backend.
|
|
668
|
+
*
|
|
669
|
+
* @param options Browser listener, admission, and backend configuration.
|
|
670
|
+
* @param dynamic The native forwarder following current application-node membership.
|
|
671
|
+
* @param creator The native subscription adapter used for creation, recovery, and cancellation.
|
|
672
|
+
* @param bindings The store retaining browser-to-native subscription bindings.
|
|
673
|
+
* @returns Request adapters and gateways sharing the configured admission policy.
|
|
674
|
+
*/
|
|
675
|
+
admission(options, dynamic, creator, bindings) {
|
|
676
|
+
const requests = BrowserServer.requests(options);
|
|
677
|
+
const admission = options.publicAccess === true
|
|
678
|
+
? { publicAccess: true }
|
|
679
|
+
: { sessions: options.sessions };
|
|
680
|
+
const unary = new UnaryGateway({
|
|
681
|
+
...(options.registry === undefined ? {} : { registry: options.registry }),
|
|
682
|
+
maxRequestBytes: 1_048_576,
|
|
683
|
+
...admission,
|
|
684
|
+
authorize: options.authorize,
|
|
685
|
+
contexts: options.contexts,
|
|
686
|
+
clock: options.clock,
|
|
687
|
+
forward: dynamic.forward.bind(dynamic),
|
|
688
|
+
});
|
|
689
|
+
const subscriptions = new SubscriptionGateway({
|
|
690
|
+
bindings,
|
|
691
|
+
...admission,
|
|
692
|
+
authorize: options.authorize,
|
|
693
|
+
contexts: options.contexts,
|
|
694
|
+
clock: options.clock,
|
|
695
|
+
creator,
|
|
696
|
+
});
|
|
697
|
+
return { requests, unary, subscriptions };
|
|
698
|
+
},
|
|
699
|
+
/**
|
|
700
|
+
* Starts discovery and rehydrates only durable subscriptions with reader identity.
|
|
701
|
+
*
|
|
702
|
+
* @param options Browser listener, admission, and backend configuration.
|
|
703
|
+
* @param running The local native listener to include in coordinated cleanup, when present.
|
|
704
|
+
* @param fixedNodes Backend nodes used when no discovery source is configured.
|
|
705
|
+
* @param dynamic The native forwarder following current application-node membership.
|
|
706
|
+
* @param creator The native subscription adapter used for creation, recovery, and cancellation.
|
|
707
|
+
* @param bindings The store retaining browser-to-native subscription bindings.
|
|
708
|
+
* @param subscriptions The gateway managing admitted browser subscriptions.
|
|
709
|
+
* @returns The discovery stop callback, or no callback for fixed membership.
|
|
710
|
+
*/
|
|
711
|
+
async startMembership(options, running, fixedNodes, dynamic, creator, bindings, subscriptions) {
|
|
712
|
+
let stopDiscovery;
|
|
713
|
+
try {
|
|
714
|
+
if (options.discovery !== undefined)
|
|
715
|
+
stopDiscovery = await BrowserServerValues.watch(options.discovery, dynamic);
|
|
716
|
+
else
|
|
717
|
+
await dynamic.reconcile(fixedNodes);
|
|
718
|
+
if (isDurableSubscriptionBindings(bindings))
|
|
719
|
+
attachDurableSubscriptionCleanup(bindings, (definition, signal) => creator.cancel({ wire: definition }, signal));
|
|
720
|
+
await BrowserServerValues.recoverBindings(options, bindings, creator, subscriptions);
|
|
721
|
+
return stopDiscovery;
|
|
722
|
+
}
|
|
723
|
+
catch (error) {
|
|
724
|
+
return BrowserServerValues.rollbackMembership(error, running, subscriptions, stopDiscovery, dynamic);
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
/**
|
|
728
|
+
* Closes discovery, admission and native listeners after recovery failure.
|
|
729
|
+
*
|
|
730
|
+
* @param error The startup failure to preserve while cleanup is attempted.
|
|
731
|
+
* @param running The local native listener to include in coordinated cleanup, when present.
|
|
732
|
+
* @param subscriptions The gateway managing admitted browser subscriptions.
|
|
733
|
+
* @param stopDiscovery The callback that stops discovery updates, when discovery is configured.
|
|
734
|
+
* @param dynamic The native forwarder following current application-node membership.
|
|
735
|
+
* @returns No value; the startup failure is rethrown with any cleanup failures.
|
|
736
|
+
*/
|
|
737
|
+
async rollbackMembership(error, running, subscriptions, stopDiscovery, dynamic) {
|
|
738
|
+
const failures = [error];
|
|
739
|
+
for (const cleanup of [
|
|
740
|
+
() => subscriptions.close(),
|
|
741
|
+
() => stopDiscovery?.(),
|
|
742
|
+
() => dynamic.close(),
|
|
743
|
+
() => running?.close(),
|
|
744
|
+
])
|
|
745
|
+
try {
|
|
746
|
+
await cleanup();
|
|
747
|
+
}
|
|
748
|
+
catch (cleanupError) {
|
|
749
|
+
failures.push(cleanupError);
|
|
750
|
+
}
|
|
751
|
+
if (failures.length === 1)
|
|
752
|
+
throw error;
|
|
753
|
+
throw new AggregateError(failures, "Browser subscription recovery startup rollback failed.");
|
|
754
|
+
},
|
|
755
|
+
/**
|
|
756
|
+
* Restores live reader-bound definitions after membership is ready.
|
|
757
|
+
*
|
|
758
|
+
* @param options Browser listener, admission, and backend configuration.
|
|
759
|
+
* @param bindings The store retaining browser-to-native subscription bindings.
|
|
760
|
+
* @param creator The native subscription adapter used for creation, recovery, and cancellation.
|
|
761
|
+
* @param subscriptions The gateway managing admitted browser subscriptions.
|
|
762
|
+
* @returns Completion after retained definitions are rehydrated and expiry is scheduled.
|
|
763
|
+
*/
|
|
764
|
+
async recoverBindings(options, bindings, creator, subscriptions) {
|
|
765
|
+
if (bindings.recoverActive === undefined)
|
|
766
|
+
return;
|
|
767
|
+
const now = options.clock.now();
|
|
768
|
+
const nowMs = Number(now.seconds.toString()) * 1_000 + Math.floor(Number(now.nanos.toString()) / 1_000_000);
|
|
769
|
+
if (!Number.isSafeInteger(nowMs))
|
|
770
|
+
return;
|
|
771
|
+
await bindings.recoverActive({
|
|
772
|
+
nowMs,
|
|
773
|
+
onDefinition: async (definition, whenExpires) => {
|
|
774
|
+
await creator.rehydrate(definition);
|
|
775
|
+
subscriptions.scheduleExpiry(whenExpires);
|
|
776
|
+
},
|
|
777
|
+
});
|
|
778
|
+
},
|
|
779
|
+
/**
|
|
780
|
+
* Binds admitted gateway services to one HTTP listener.
|
|
781
|
+
*
|
|
782
|
+
* @param input Gateway services and HTTP routing policy for this listener.
|
|
783
|
+
* @returns The unbound HTTP server serving the admitted Spine routes.
|
|
784
|
+
*/
|
|
785
|
+
createListener(input) {
|
|
786
|
+
const services = createNativeGatewayServices({
|
|
787
|
+
unary: input.unary,
|
|
788
|
+
subscriptions: input.subscriptions,
|
|
789
|
+
requests: input.requests,
|
|
790
|
+
});
|
|
791
|
+
const handler = connectNodeAdapter({
|
|
792
|
+
routes: (router) => {
|
|
793
|
+
router.service(AuthenticationService, services.authentication);
|
|
794
|
+
router.service(CommandService, services.command);
|
|
795
|
+
router.service(QueryService, services.query);
|
|
796
|
+
router.service(SubscriptionService, services.subscription);
|
|
797
|
+
},
|
|
798
|
+
readMaxBytes: input.options.readMaxBytes,
|
|
799
|
+
writeMaxBytes: input.options.writeMaxBytes,
|
|
800
|
+
});
|
|
801
|
+
const context = { ...input, handler };
|
|
802
|
+
return http.createServer((request, response) => {
|
|
803
|
+
BrowserServerValues.routeRequest(request, response, context);
|
|
804
|
+
});
|
|
805
|
+
},
|
|
806
|
+
/**
|
|
807
|
+
* Applies drain, authentication and RPC routing to one HTTP request.
|
|
808
|
+
*
|
|
809
|
+
* @param request The incoming HTTP request.
|
|
810
|
+
* @param response The HTTP response to complete with the route outcome.
|
|
811
|
+
* @param context Listener admission policy, active requests, and the RPC routing callback.
|
|
812
|
+
*/
|
|
813
|
+
routeRequest(request, response, context) {
|
|
814
|
+
if (context.draining()) {
|
|
815
|
+
response.statusCode = 503;
|
|
816
|
+
response.end();
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const auth = context.authRoutes.get(request.url ?? "");
|
|
820
|
+
if (auth !== undefined) {
|
|
821
|
+
BrowserServerValues.authRequest(request, response, auth, context);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
BrowserServerValues.rpcRequest(request, response, context);
|
|
825
|
+
},
|
|
826
|
+
/**
|
|
827
|
+
* Handles the configured browser authentication route and its preflight.
|
|
828
|
+
*
|
|
829
|
+
* @param request The incoming HTTP request.
|
|
830
|
+
* @param response The HTTP response to complete with the route outcome.
|
|
831
|
+
* @param auth The authentication route matched by the request path.
|
|
832
|
+
* @param context Listener admission policy, active requests, and the RPC routing callback.
|
|
833
|
+
*/
|
|
834
|
+
authRequest(request, response, auth, context) {
|
|
835
|
+
if (request.method === auth.method) {
|
|
836
|
+
if (context.activeAuth.size >= context.maxActiveAuthRequests) {
|
|
837
|
+
response.statusCode = 503;
|
|
838
|
+
response.end();
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
void BrowserServer.dispatchAuth(request, response, auth, context.activeAuth, context.options.writeMaxBytes);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const origin = request.headers.origin;
|
|
845
|
+
if (request.method === "OPTIONS" && origin !== undefined && auth.origins.includes(origin)) {
|
|
846
|
+
response.setHeader("access-control-allow-origin", origin);
|
|
847
|
+
response.setHeader("access-control-allow-methods", `${auth.method},OPTIONS`);
|
|
848
|
+
response.statusCode = 204;
|
|
849
|
+
}
|
|
850
|
+
else
|
|
851
|
+
response.statusCode = request.method === "OPTIONS" ? 403 : 405;
|
|
852
|
+
response.end();
|
|
853
|
+
},
|
|
854
|
+
/**
|
|
855
|
+
* Checks browser origin admission before a Connect or gRPC request.
|
|
856
|
+
*
|
|
857
|
+
* @param request The incoming HTTP request.
|
|
858
|
+
* @param response The HTTP response to complete with the route outcome.
|
|
859
|
+
* @param context Listener admission policy, active requests, and the RPC routing callback.
|
|
860
|
+
*/
|
|
861
|
+
rpcRequest(request, response, context) {
|
|
862
|
+
const origin = request.headers.origin;
|
|
863
|
+
if (origin === undefined || !context.origins.has(origin)) {
|
|
864
|
+
response.statusCode = 403;
|
|
865
|
+
response.end();
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
response.setHeader("access-control-allow-origin", origin);
|
|
869
|
+
response.setHeader("access-control-allow-credentials", "true");
|
|
870
|
+
response.setHeader("access-control-expose-headers", "grpc-status,grpc-message");
|
|
871
|
+
response.setHeader("vary", "Origin");
|
|
872
|
+
if (request.method === "OPTIONS") {
|
|
873
|
+
response.setHeader("access-control-allow-methods", "POST,OPTIONS");
|
|
874
|
+
response.setHeader("access-control-allow-headers", "authorization,content-type,connect-protocol-version,x-csrf-token");
|
|
875
|
+
response.setHeader("access-control-max-age", "600");
|
|
876
|
+
response.statusCode = 204;
|
|
877
|
+
response.end();
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
context.handler(request, response);
|
|
881
|
+
},
|
|
882
|
+
/**
|
|
883
|
+
* Subscribes the native forwarder to application-node discovery changes.
|
|
884
|
+
*
|
|
885
|
+
* @param source The source of application-node membership changes.
|
|
886
|
+
* @param forwarder The native forwarder to reconcile when membership changes.
|
|
887
|
+
* @returns The callback that stops watching node discovery.
|
|
888
|
+
*/
|
|
573
889
|
async watch(source, forwarder) {
|
|
574
890
|
return await source.watch((nodes) => {
|
|
575
891
|
void forwarder.reconcile(nodes);
|
|
576
892
|
});
|
|
577
893
|
},
|
|
894
|
+
/**
|
|
895
|
+
* Returns the local native listener or rejects an absent backend.
|
|
896
|
+
*
|
|
897
|
+
* @param value The possibly absent local native listener.
|
|
898
|
+
* @returns The supplied listener, or throws when it is absent.
|
|
899
|
+
*/
|
|
578
900
|
requiredRunning(value) {
|
|
579
901
|
if (value === undefined)
|
|
580
902
|
throw new Error("Browser server local backend is absent.");
|
|
581
903
|
return value;
|
|
582
904
|
},
|
|
905
|
+
/**
|
|
906
|
+
* Checks IPv4 loopback addresses and the supported IPv6 loopback forms.
|
|
907
|
+
*
|
|
908
|
+
* @param host The address to test for local loopback membership.
|
|
909
|
+
* @returns Whether the address denotes a supported loopback interface.
|
|
910
|
+
*/
|
|
583
911
|
isLoopback(host) {
|
|
584
912
|
if (host === "::1" || host === "::ffff:127.0.0.1")
|
|
585
913
|
return true;
|
|
586
914
|
const match = /^127\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/u.exec(host);
|
|
587
915
|
return match?.slice(1).every((part) => Number(part) <= 255) === true;
|
|
588
916
|
},
|
|
917
|
+
/**
|
|
918
|
+
* Calls every supplied startup cleanup and preserves any cleanup failures.
|
|
919
|
+
*
|
|
920
|
+
* @param error The startup failure to preserve while cleanup is attempted.
|
|
921
|
+
* @param cleanups Startup cleanup callbacks to attempt in the supplied order.
|
|
922
|
+
* @returns No value; the original failure or an aggregate of startup and cleanup failures is thrown.
|
|
923
|
+
*/
|
|
589
924
|
async rollback(error, cleanups) {
|
|
590
925
|
const failures = [error];
|
|
591
926
|
for (const cleanup of cleanups)
|
|
@@ -599,17 +934,35 @@ const BrowserServerValues = Object.freeze({
|
|
|
599
934
|
throw error;
|
|
600
935
|
throw new AggregateError(failures, "Browser server startup rollback failed.");
|
|
601
936
|
},
|
|
937
|
+
/**
|
|
938
|
+
* Returns the first native subscription client or rejects an empty configuration.
|
|
939
|
+
*
|
|
940
|
+
* @param values Native subscription clients in configured order.
|
|
941
|
+
* @returns The first configured client, or throws when none exists.
|
|
942
|
+
*/
|
|
602
943
|
firstCreator(values) {
|
|
603
944
|
const creator = values[0];
|
|
604
945
|
if (creator === undefined)
|
|
605
946
|
throw new Error("Browser server backend is absent.");
|
|
606
947
|
return creator;
|
|
607
948
|
},
|
|
949
|
+
/**
|
|
950
|
+
* Returns an already running native listener from the backend configuration.
|
|
951
|
+
*
|
|
952
|
+
* @param native The native backend to expose through this browser host.
|
|
953
|
+
* @returns The supplied running listener, or undefined for URL-based or absent backends.
|
|
954
|
+
*/
|
|
608
955
|
running(native) {
|
|
609
956
|
return native === undefined || typeof native === "string" || Array.isArray(native)
|
|
610
957
|
? undefined
|
|
611
958
|
: native;
|
|
612
959
|
},
|
|
960
|
+
/**
|
|
961
|
+
* Reads exactly one of the singular or plural backend URL options.
|
|
962
|
+
*
|
|
963
|
+
* @param backend The singular or plural backend URL configuration to inspect.
|
|
964
|
+
* @returns The configured backend origins without applying URL validation.
|
|
965
|
+
*/
|
|
613
966
|
backendUrlsFor(backend) {
|
|
614
967
|
const source = backend;
|
|
615
968
|
if (typeof source.baseUrl === "string" && source.baseUrls === undefined)
|
|
@@ -623,6 +976,9 @@ const BrowserServerValues = Object.freeze({
|
|
|
623
976
|
throw new Error("Server browser backend must configure exactly one of baseUrl or baseUrls.");
|
|
624
977
|
},
|
|
625
978
|
});
|
|
979
|
+
/**
|
|
980
|
+
* Closes the browser listener, discovery, admission and native backend together.
|
|
981
|
+
*/
|
|
626
982
|
class RunningBrowserServer {
|
|
627
983
|
#server;
|
|
628
984
|
#subscriptions;
|
|
@@ -641,6 +997,18 @@ class RunningBrowserServer {
|
|
|
641
997
|
#discoveryStopped = false;
|
|
642
998
|
#dynamicClosed = false;
|
|
643
999
|
#nativeClosed = false;
|
|
1000
|
+
/**
|
|
1001
|
+
* Stores the running browser resources and exposes their bound address.
|
|
1002
|
+
*
|
|
1003
|
+
* @param server The HTTP listener being started or stopped.
|
|
1004
|
+
* @param native The native backend to expose through this browser host.
|
|
1005
|
+
* @param subscriptions The gateway managing admitted browser subscriptions.
|
|
1006
|
+
* @param address The actual address returned when the browser listener bound successfully.
|
|
1007
|
+
* @param activeAuth Controllers tracked for cancellation during browser-host shutdown.
|
|
1008
|
+
* @param onDrain Stops admission of new browser requests before resources are closed.
|
|
1009
|
+
* @param stopDiscovery The callback that stops discovery updates, when discovery is configured.
|
|
1010
|
+
* @param dynamic The native forwarder following current application-node membership.
|
|
1011
|
+
*/
|
|
644
1012
|
constructor(server, native, subscriptions, address, activeAuth, onDrain, stopDiscovery, dynamic) {
|
|
645
1013
|
this.#server = server;
|
|
646
1014
|
this.#native = native;
|
|
@@ -654,6 +1022,11 @@ class RunningBrowserServer {
|
|
|
654
1022
|
const host = this.host.includes(":") && !this.host.startsWith("[") ? `[${this.host}]` : this.host;
|
|
655
1023
|
this.baseUrl = `http://${host}:${this.port.toString()}`;
|
|
656
1024
|
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Returns a shared promise for concurrent shutdown attempts and permits retry after a failed attempt.
|
|
1027
|
+
*
|
|
1028
|
+
* @returns The shared shutdown attempt, which rejects if any cleanup phase fails.
|
|
1029
|
+
*/
|
|
657
1030
|
close() {
|
|
658
1031
|
this.#closed ??= this.#closeOnce().catch((error) => {
|
|
659
1032
|
this.#closed = undefined;
|
|
@@ -661,6 +1034,11 @@ class RunningBrowserServer {
|
|
|
661
1034
|
});
|
|
662
1035
|
return this.#closed;
|
|
663
1036
|
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Stops admission and attempts each unfinished shutdown phase.
|
|
1039
|
+
*
|
|
1040
|
+
* @returns Completion after all unfinished phases are attempted; rejects with collected failures.
|
|
1041
|
+
*/
|
|
664
1042
|
async #closeOnce() {
|
|
665
1043
|
this.#onDrain();
|
|
666
1044
|
for (const controller of this.#activeAuth)
|
|
@@ -690,6 +1068,14 @@ class RunningBrowserServer {
|
|
|
690
1068
|
throw primary;
|
|
691
1069
|
}
|
|
692
1070
|
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Marks successful shutdown work and collects failures without stopping later phases.
|
|
1073
|
+
*
|
|
1074
|
+
* @param operation An unfinished shutdown operation, or undefined when there is nothing to await.
|
|
1075
|
+
* @param onSuccess Records completion so a later shutdown retry skips this phase.
|
|
1076
|
+
* @param failures The shared collection receiving failures from shutdown phases.
|
|
1077
|
+
* @returns Completion after success is recorded or the failure is collected.
|
|
1078
|
+
*/
|
|
693
1079
|
async #phase(operation, onSuccess, failures) {
|
|
694
1080
|
if (operation === undefined)
|
|
695
1081
|
return;
|
|
@@ -701,6 +1087,11 @@ class RunningBrowserServer {
|
|
|
701
1087
|
failures.push(error);
|
|
702
1088
|
}
|
|
703
1089
|
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Returns the in-flight listener shutdown and resets it after settlement.
|
|
1092
|
+
*
|
|
1093
|
+
* @returns The current listener-close attempt or a newly started attempt.
|
|
1094
|
+
*/
|
|
704
1095
|
#closeListenerPhase() {
|
|
705
1096
|
const current = this.#listenerClose;
|
|
706
1097
|
if (current !== undefined)
|