@spine-event-engine/server 2.0.0-snapshot.25 → 2.0.0-snapshot.28
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 +104 -16
- package/REFERENCE.md +486 -57
- 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-ai-runtime.d.ts +1 -1
- package/dist/agent/agent-live-ai-runtime.js +1 -1
- package/dist/agent/agent-live-budget.d.ts +1 -1
- package/dist/agent/agent-live-budget.js +1 -1
- package/dist/agent/agent-live-read-runtime.d.ts +13 -2
- package/dist/agent/agent-live-read-runtime.d.ts.map +1 -1
- package/dist/agent/agent-live-read-runtime.js +6 -5
- package/dist/agent/agent-live-read-runtime.js.map +1 -1
- package/dist/browser/browser-admission.d.ts +47 -0
- package/dist/browser/browser-admission.d.ts.map +1 -0
- package/dist/browser/browser-admission.js +200 -0
- package/dist/browser/browser-admission.js.map +1 -0
- package/dist/browser/browser-server.d.ts +171 -1
- package/dist/browser/browser-server.d.ts.map +1 -1
- package/dist/browser/browser-server.js +1027 -303
- 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/index.d.ts +1 -1
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/options.d.ts +53 -2
- package/dist/browser/options.d.ts.map +1 -1
- package/dist/bus/command-bus.d.ts +9 -1
- package/dist/bus/command-bus.d.ts.map +1 -1
- package/dist/bus/command-bus.js +75 -10
- 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 +9 -1
- package/dist/bus/event-bus.d.ts.map +1 -1
- package/dist/bus/event-bus.js +136 -9
- package/dist/bus/event-bus.js.map +1 -1
- package/dist/context/bounded-context.d.ts +59 -39
- package/dist/context/bounded-context.d.ts.map +1 -1
- package/dist/context/bounded-context.js +277 -262
- package/dist/context/bounded-context.js.map +1 -1
- package/dist/context/entity-inbox.d.ts +12 -17
- package/dist/context/entity-inbox.d.ts.map +1 -1
- package/dist/context/entity-inbox.js +106 -119
- package/dist/context/entity-inbox.js.map +1 -1
- package/dist/context/local-inbox-handoff.d.ts +13 -13
- package/dist/context/local-inbox-handoff.d.ts.map +1 -1
- package/dist/context/local-inbox-handoff.js +60 -32
- package/dist/context/local-inbox-handoff.js.map +1 -1
- package/dist/context/persistent-tenant-index.d.ts +81 -0
- package/dist/context/persistent-tenant-index.d.ts.map +1 -0
- package/dist/context/persistent-tenant-index.js +270 -0
- package/dist/context/persistent-tenant-index.js.map +1 -0
- package/dist/context/projection-handoff.d.ts +9 -9
- package/dist/context/projection-handoff.d.ts.map +1 -1
- package/dist/context/projection-handoff.js +31 -23
- package/dist/context/projection-handoff.js.map +1 -1
- package/dist/context/tenant-index.d.ts +7 -6
- package/dist/context/tenant-index.d.ts.map +1 -1
- package/dist/context/tenant-index.js +2 -188
- package/dist/context/tenant-index.js.map +1 -1
- package/dist/context/tenant-unknown-error.d.ts +2 -2
- package/dist/context/tenant-unknown-error.d.ts.map +1 -1
- package/dist/context/tenant-unknown-error.js +4 -1
- package/dist/context/tenant-unknown-error.js.map +1 -1
- package/dist/delivery/delivery-supervisor.d.ts.map +1 -1
- package/dist/delivery/delivery-supervisor.js +12 -3
- package/dist/delivery/delivery-supervisor.js.map +1 -1
- package/dist/delivery/delivery.d.ts +4 -3
- package/dist/delivery/delivery.d.ts.map +1 -1
- package/dist/delivery/delivery.js +82 -34
- package/dist/delivery/delivery.js.map +1 -1
- package/dist/delivery/reception-observation.d.ts +3 -3
- package/dist/delivery/reception-observation.js +2 -2
- 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 +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/repository/command-routing.d.ts +1 -1
- package/dist/repository/command-routing.js +1 -1
- package/dist/repository/event-routing.d.ts +1 -1
- package/dist/repository/event-routing.js +1 -1
- package/dist/repository/repository.d.ts +61 -26
- package/dist/repository/repository.d.ts.map +1 -1
- package/dist/repository/repository.js +612 -306
- package/dist/repository/repository.js.map +1 -1
- package/dist/repository/state-update-routing.d.ts +1 -1
- package/dist/repository/state-update-routing.js +1 -1
- package/dist/runtime/signal-publisher.d.ts +23 -8
- package/dist/runtime/signal-publisher.d.ts.map +1 -1
- package/dist/runtime/signal-publisher.js +11 -8
- package/dist/runtime/signal-publisher.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 +744 -108
- 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 +23 -5
- package/dist/server/server-environment.d.ts.map +1 -1
- package/dist/server/server-environment.js +89 -24
- 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 +112 -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/native-operation.d.ts +34 -0
- package/dist/services/native-operation.d.ts.map +1 -0
- package/dist/services/native-operation.js +77 -0
- package/dist/services/native-operation.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 +42 -7
- package/dist/services/spine-services.d.ts.map +1 -1
- package/dist/services/spine-services.js +1005 -175
- package/dist/services/spine-services.js.map +1 -1
- package/dist/services/trusted-reader-ingress.d.ts +110 -0
- package/dist/services/trusted-reader-ingress.d.ts.map +1 -0
- package/dist/services/trusted-reader-ingress.js +102 -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 +81 -43
- package/dist/stand/stand.d.ts.map +1 -1
- package/dist/stand/stand.js +411 -55
- 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 +16 -14
|
@@ -15,17 +15,23 @@ import * as http2 from "node:http2";
|
|
|
15
15
|
import { randomUUID } from "node:crypto";
|
|
16
16
|
import { clone, create, equals, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
17
17
|
import { EmptySchema } from "@bufbuild/protobuf/wkt";
|
|
18
|
-
import { Code, ConnectError, createClient } from "@connectrpc/connect";
|
|
18
|
+
import { Code, ConnectError, createClient, } from "@connectrpc/connect";
|
|
19
19
|
import { connectNodeAdapter, createGrpcTransport, Http2SessionManager, } from "@connectrpc/connect-node";
|
|
20
20
|
import { BackendMembershipKernel, } from "@spine-event-engine/deployment/spi/backend-membership";
|
|
21
21
|
import { SUBSCRIPTION_ACTIVATION_HANDSHAKE_MS } from "@spine-event-engine/core/spi/subscription-lifecycle";
|
|
22
|
+
import { Validate } from "@spine-event-engine/core";
|
|
23
|
+
import { ReaderForwarding, } from "@spine-event-engine/core/spi/reader-forwarding";
|
|
24
|
+
import { ReaderIdentities, snapshotTrustedReader, } from "@spine-event-engine/core/spi/trusted-reader";
|
|
22
25
|
import { AckSchema, ResponseSchema, StatusSchema, } from "@spine-event-engine/proto";
|
|
23
|
-
import { QueryResponseSchema, CommandService, QueryService, SubscriptionIdSchema, SubscriptionSchema, SubscriptionService, SubscriptionUpdateSchema, TopicSchema, } from "@spine-event-engine/proto/client";
|
|
26
|
+
import { AppQueryResponseSchema, AppQuerySchema, QueryResponseSchema, QuerySchema, CommandService, QueryService, SubscriptionIdSchema, SubscriptionSchema, SubscriptionService, SubscriptionUpdateSchema, TopicSchema, } from "@spine-event-engine/proto/client";
|
|
27
|
+
import { ReadRequestStatus } from "../services/read-request-status.js";
|
|
28
|
+
import { TrustedReaderIngress, } from "../services/trusted-reader-ingress.js";
|
|
29
|
+
import { NativeOperation } from "../services/native-operation.js";
|
|
30
|
+
import { ServerEnvironment } from "./server-environment.js";
|
|
24
31
|
const defaultHost = "127.0.0.1";
|
|
25
32
|
const defaultPort = 0;
|
|
26
33
|
const defaultMessageMaxBytes = 4_194_304;
|
|
27
34
|
const gracefulSessionDrainMs = 100;
|
|
28
|
-
const subscriptionQueueLimit = 100;
|
|
29
35
|
/**
|
|
30
36
|
* Hosts generated unary services in front of managed complete replicas.
|
|
31
37
|
*
|
|
@@ -34,6 +40,13 @@ const subscriptionQueueLimit = 100;
|
|
|
34
40
|
export class NodeCoordinator {
|
|
35
41
|
// prettier-ignore
|
|
36
42
|
#members;
|
|
43
|
+
#readerAdmission;
|
|
44
|
+
#forwardingProof;
|
|
45
|
+
#readers = new Map();
|
|
46
|
+
#children = new Map();
|
|
47
|
+
#logicalForChild = new Map();
|
|
48
|
+
#readMaxBytes;
|
|
49
|
+
#subscriptionQueueCapacity;
|
|
37
50
|
#unaryKernel;
|
|
38
51
|
#subscriptionKernel;
|
|
39
52
|
#server;
|
|
@@ -59,6 +72,7 @@ export class NodeCoordinator {
|
|
|
59
72
|
#close;
|
|
60
73
|
#networkClose;
|
|
61
74
|
#draining = false;
|
|
75
|
+
#admissionShutdown = new AbortController();
|
|
62
76
|
#membershipReconciliation = Promise.resolve();
|
|
63
77
|
/**
|
|
64
78
|
* Creates the native node coordinator around its HTTP/2 listener and member source.
|
|
@@ -67,26 +81,48 @@ export class NodeCoordinator {
|
|
|
67
81
|
* @param server The HTTP/2 server.
|
|
68
82
|
* @param address The listening address.
|
|
69
83
|
* @param sessions Tracked HTTP/2 sessions.
|
|
84
|
+
* @param subscriptionQueueCapacity Resolved bound for each managed subscriber.
|
|
70
85
|
*/
|
|
71
|
-
constructor(options, server, address, sessions) {
|
|
86
|
+
constructor(options, server, address, sessions, subscriptionQueueCapacity) {
|
|
72
87
|
this.#members = options.members;
|
|
73
|
-
this.#
|
|
74
|
-
this.#
|
|
88
|
+
this.#readerAdmission = options.readerAdmission;
|
|
89
|
+
this.#forwardingProof = options.forwardingProof;
|
|
90
|
+
this.#readMaxBytes = options.readMaxBytes ?? defaultMessageMaxBytes;
|
|
91
|
+
this.#subscriptionQueueCapacity = subscriptionQueueCapacity;
|
|
92
|
+
this.#unaryKernel = new BackendMembershipKernel(NodeCoordinatorValues.unaryKernelOptions(undefined, this.#forwardingProof, this.#readMaxBytes, (bytes) => this.#readerFor(bytes)));
|
|
93
|
+
this.#subscriptionKernel = this.#makeSubscriptionKernel(options.members);
|
|
75
94
|
this.#server = server;
|
|
76
95
|
this.#sessions = sessions;
|
|
77
96
|
this.host = address.address;
|
|
78
97
|
this.port = address.port;
|
|
79
98
|
this.baseUrl = `http://${NodeCoordinatorValues.formatHostForUrl(this.host)}:${this.port.toString()}`;
|
|
80
99
|
const reconcile = () => {
|
|
81
|
-
this.#
|
|
82
|
-
.then(() => this.#reconcile(), () => this.#reconcile())
|
|
83
|
-
.catch(() => undefined);
|
|
100
|
+
this.#scheduleReconciliation();
|
|
84
101
|
};
|
|
85
102
|
this.#stopMembers = [
|
|
86
103
|
options.members.onReadyMembersChange(reconcile),
|
|
87
104
|
options.members.onRelayMembersChange(reconcile),
|
|
88
105
|
];
|
|
89
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Builds the subscription kernel with reader provenance for each child.
|
|
109
|
+
*
|
|
110
|
+
* @param members The available backend member source.
|
|
111
|
+
* @returns The native subscription kernel.
|
|
112
|
+
*/
|
|
113
|
+
#makeSubscriptionKernel(members) {
|
|
114
|
+
return new BackendMembershipKernel(NodeCoordinatorValues.unaryKernelOptions(members, this.#forwardingProof, this.#readMaxBytes, (bytes) => this.#readerFor(bytes), (definition, child) => {
|
|
115
|
+
this.#rememberChild(definition, child);
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Serializes membership refresh after readiness or relay changes.
|
|
120
|
+
*/
|
|
121
|
+
#scheduleReconciliation() {
|
|
122
|
+
this.#membershipReconciliation = this.#membershipReconciliation
|
|
123
|
+
.then(() => this.#reconcile(), () => this.#reconcile())
|
|
124
|
+
.catch(() => undefined);
|
|
125
|
+
}
|
|
90
126
|
/**
|
|
91
127
|
* Opens one generated unary-service listener.
|
|
92
128
|
*
|
|
@@ -94,37 +130,44 @@ export class NodeCoordinator {
|
|
|
94
130
|
* @returns The running private Coordinator.
|
|
95
131
|
*/
|
|
96
132
|
static async open(options) {
|
|
133
|
+
const subscriptionQueueCapacity = ServerEnvironment.instance().subscriptionQueueCapacity;
|
|
97
134
|
const { host, port, readMaxBytes, writeMaxBytes } = this.#listenerOptions(options);
|
|
98
135
|
// eslint-disable-next-line prefer-const -- routes capture the Coordinator before the listener exists.
|
|
99
136
|
let coordinator;
|
|
100
137
|
const sessions = new Set();
|
|
101
138
|
const server = http2.createServer(connectNodeAdapter({
|
|
102
139
|
routes: (router) => {
|
|
103
|
-
router
|
|
104
|
-
post: async (command, context) => {
|
|
105
|
-
return coordinator.#post(command, context);
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
router.service(QueryService, {
|
|
109
|
-
read: async (query, context) => {
|
|
110
|
-
return coordinator.#read(query, context);
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
router.service(SubscriptionService, {
|
|
114
|
-
subscribe: (topic, context) => coordinator.#subscribe(topic, context),
|
|
115
|
-
activate: (subscription, context) => coordinator.#activate(subscription, context),
|
|
116
|
-
cancel: (subscription, context) => coordinator.#cancel(subscription, context),
|
|
117
|
-
});
|
|
140
|
+
this.#routes(router, () => coordinator);
|
|
118
141
|
},
|
|
119
142
|
readMaxBytes,
|
|
120
143
|
writeMaxBytes,
|
|
121
144
|
}));
|
|
122
145
|
this.#trackSessions(server, sessions);
|
|
123
146
|
const address = await NodeCoordinatorValues.listen(server, host, port);
|
|
124
|
-
coordinator = new NodeCoordinator(options, server, address, sessions);
|
|
147
|
+
coordinator = new NodeCoordinator(options, server, address, sessions, subscriptionQueueCapacity);
|
|
125
148
|
await coordinator.#reconcile();
|
|
126
149
|
return coordinator;
|
|
127
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Registers native service routes after the listener receives its Coordinator instance.
|
|
153
|
+
*
|
|
154
|
+
* @param router Connect router for the private listener.
|
|
155
|
+
* @param current Returns the initialized Coordinator at request time.
|
|
156
|
+
*/
|
|
157
|
+
static #routes(router, current) {
|
|
158
|
+
router.service(CommandService, {
|
|
159
|
+
post: (command, context) => current().#post(command, context),
|
|
160
|
+
});
|
|
161
|
+
router.service(QueryService, {
|
|
162
|
+
read: (query, context) => current().#read(query, context),
|
|
163
|
+
execute: (query, context) => current().#execute(query, context),
|
|
164
|
+
});
|
|
165
|
+
router.service(SubscriptionService, {
|
|
166
|
+
subscribe: (topic, context) => current().#subscribe(topic, context),
|
|
167
|
+
activate: (subscription, context) => current().#activate(subscription, context),
|
|
168
|
+
cancel: (subscription, context) => current().#cancel(subscription, context),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
128
171
|
/**
|
|
129
172
|
* Validates private listener and message limits before opening the server.
|
|
130
173
|
*
|
|
@@ -157,6 +200,7 @@ export class NodeCoordinator {
|
|
|
157
200
|
* @returns Completion after bounded Coordinator cleanup.
|
|
158
201
|
*/
|
|
159
202
|
close() {
|
|
203
|
+
this.#admissionShutdown.abort();
|
|
160
204
|
this.#close ??= this.#closeOnce().catch((error) => {
|
|
161
205
|
this.#close = undefined;
|
|
162
206
|
throw error;
|
|
@@ -181,46 +225,238 @@ export class NodeCoordinator {
|
|
|
181
225
|
await this.#membershipReconciliation;
|
|
182
226
|
await this.#subscriptionKernel.close();
|
|
183
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Admits a command reader and forwards the command to a managed child.
|
|
230
|
+
*
|
|
231
|
+
* @param command Command envelope from the native client.
|
|
232
|
+
* @param context The trusted context for this operation.
|
|
233
|
+
* @returns Child acknowledgment after forwarding.
|
|
234
|
+
*/
|
|
184
235
|
async #post(command, context) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
236
|
+
return this.#runAdmitted("spine.client.CommandService", "Post", toBinary(CommandService.method.post.input, command), context, async (reader, signal) => {
|
|
237
|
+
const request = {
|
|
238
|
+
kind: "command",
|
|
239
|
+
command,
|
|
240
|
+
context,
|
|
241
|
+
reader,
|
|
242
|
+
signal,
|
|
243
|
+
response: create(AckSchema),
|
|
244
|
+
};
|
|
245
|
+
await this.#forward(request);
|
|
246
|
+
return request.response;
|
|
247
|
+
});
|
|
193
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Admits a read reader and forwards the query to a managed child.
|
|
251
|
+
*
|
|
252
|
+
* @param query Query envelope from the native client.
|
|
253
|
+
* @param context The trusted context for this operation.
|
|
254
|
+
* @returns Child read response after forwarding.
|
|
255
|
+
*/
|
|
194
256
|
async #read(query, context) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
257
|
+
return this.#runAdmitted("spine.client.QueryService", "Read", toBinary(QuerySchema, query), context, async (reader, signal) => {
|
|
258
|
+
ReadRequestStatus.check("query", () => Validate.check(QuerySchema, query));
|
|
259
|
+
const request = {
|
|
260
|
+
kind: "query",
|
|
261
|
+
query,
|
|
262
|
+
context,
|
|
263
|
+
reader,
|
|
264
|
+
signal,
|
|
265
|
+
response: create(QueryResponseSchema),
|
|
266
|
+
};
|
|
267
|
+
await this.#forward(request);
|
|
268
|
+
return request.response;
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Admits an App Query and forwards it to a ready application replica.
|
|
273
|
+
*
|
|
274
|
+
* @param query App Query envelope.
|
|
275
|
+
* @param context Native RPC context.
|
|
276
|
+
* @returns The application replica's response.
|
|
277
|
+
*/
|
|
278
|
+
async #execute(query, context) {
|
|
279
|
+
return this.#runAdmitted("spine.client.QueryService", "Execute", toBinary(QueryService.method.execute.input, query), context, async (reader, signal) => {
|
|
280
|
+
ReadRequestStatus.check("query", () => Validate.check(AppQuerySchema, query));
|
|
281
|
+
const request = {
|
|
282
|
+
kind: "app-query",
|
|
283
|
+
query,
|
|
284
|
+
context,
|
|
285
|
+
reader,
|
|
286
|
+
signal,
|
|
287
|
+
response: create(AppQueryResponseSchema),
|
|
288
|
+
};
|
|
289
|
+
await this.#forward(request);
|
|
290
|
+
return request.response;
|
|
291
|
+
});
|
|
203
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Verifies forwarded or direct reader facts at the managed ingress.
|
|
295
|
+
*
|
|
296
|
+
* @param service Canonical RPC service.
|
|
297
|
+
* @param method Canonical RPC method.
|
|
298
|
+
* @param request Canonical request bytes.
|
|
299
|
+
* @param context Native transport context.
|
|
300
|
+
* @param signal Host-managed operation lifetime.
|
|
301
|
+
* @returns Captured reader admission and invalidation.
|
|
302
|
+
*/
|
|
303
|
+
#admit(service, method, request, context, signal) {
|
|
304
|
+
return TrustedReaderIngress.admit({
|
|
305
|
+
service,
|
|
306
|
+
method,
|
|
307
|
+
request,
|
|
308
|
+
headers: context.requestHeader,
|
|
309
|
+
peer: context,
|
|
310
|
+
signal,
|
|
311
|
+
maxBytes: this.#readMaxBytes,
|
|
312
|
+
...this.#readerAdmission,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Holds one unary operation through admission and child work.
|
|
317
|
+
*
|
|
318
|
+
* @typeParam T Native response type.
|
|
319
|
+
* @param service Canonical RPC service.
|
|
320
|
+
* @param method Canonical RPC method.
|
|
321
|
+
* @param bytes Canonical request bytes.
|
|
322
|
+
* @param context Original peer metadata.
|
|
323
|
+
* @param work Verified action using the effective signal.
|
|
324
|
+
* @returns Result while admission remains valid.
|
|
325
|
+
*/
|
|
326
|
+
async #runAdmitted(service, method, bytes, context, work) {
|
|
327
|
+
const operation = new NativeOperation(context.signal, this.#admissionShutdown.signal);
|
|
328
|
+
try {
|
|
329
|
+
const admission = await this.#admit(service, method, bytes, context, operation.signal);
|
|
330
|
+
operation.link(admission.invalidated);
|
|
331
|
+
operation.check();
|
|
332
|
+
const result = await work(admission.reader, operation.signal);
|
|
333
|
+
operation.check();
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
finally {
|
|
337
|
+
operation.close();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Validates and retains a reader-bound subscription definition.
|
|
342
|
+
*
|
|
343
|
+
* @param topic Framework subscription Topic.
|
|
344
|
+
* @param context The trusted context for this operation.
|
|
345
|
+
* @returns The new subscription definition.
|
|
346
|
+
*/
|
|
204
347
|
async #subscribe(topic, context) {
|
|
348
|
+
const operation = new NativeOperation(context.signal, this.#admissionShutdown.signal);
|
|
349
|
+
try {
|
|
350
|
+
const admission = await this.#admit("spine.client.SubscriptionService", "Subscribe", toBinary(TopicSchema, topic), context, operation.signal);
|
|
351
|
+
operation.link(admission.invalidated);
|
|
352
|
+
operation.check();
|
|
353
|
+
const subscription = await this.#subscribeReader(topic, admission.reader, operation.signal);
|
|
354
|
+
try {
|
|
355
|
+
operation.check();
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
return await this.#compensateSubscribe(subscription, error);
|
|
359
|
+
}
|
|
360
|
+
return subscription;
|
|
361
|
+
}
|
|
362
|
+
finally {
|
|
363
|
+
operation.close();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Removes a committed definition after a failed final admission check.
|
|
368
|
+
*
|
|
369
|
+
* @param subscription Created logical definition.
|
|
370
|
+
* @param error Final admission failure.
|
|
371
|
+
* @returns Rejected completion after conditional cleanup.
|
|
372
|
+
*/
|
|
373
|
+
async #compensateSubscribe(subscription, error) {
|
|
374
|
+
this.#clearPendingActivation(subscription);
|
|
375
|
+
try {
|
|
376
|
+
await this.#cancelDefinition(toBinary(SubscriptionSchema, subscription), new AbortController().signal);
|
|
377
|
+
}
|
|
378
|
+
catch (cleanup) {
|
|
379
|
+
throw new AggregateError([error, cleanup], "Coordinator Subscribe cleanup failed.");
|
|
380
|
+
}
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Creates a reader-bound definition under its native operation lifetime.
|
|
385
|
+
*
|
|
386
|
+
* @param topic Framework subscription Topic.
|
|
387
|
+
* @param reader Detached authenticated facts.
|
|
388
|
+
* @param signal Effective cancellation.
|
|
389
|
+
* @returns Created logical definition.
|
|
390
|
+
*/
|
|
391
|
+
async #subscribeReader(topic, reader, signal) {
|
|
392
|
+
ReadRequestStatus.check("topic", () => Validate.check(TopicSchema, topic));
|
|
205
393
|
if (this.#draining)
|
|
206
394
|
throw new ConnectError("Managed application is closing.", Code.Unavailable);
|
|
207
395
|
const subscription = create(SubscriptionSchema, {
|
|
208
396
|
id: create(SubscriptionIdSchema, { value: `s-${randomUUID()}` }),
|
|
209
397
|
topic,
|
|
210
398
|
});
|
|
399
|
+
const id = subscription.id?.value;
|
|
400
|
+
if (id === undefined)
|
|
401
|
+
throw new Error("Created subscription has no ID.");
|
|
402
|
+
const definition = toBinary(SubscriptionSchema, subscription);
|
|
403
|
+
this.#readers.set(id, reader);
|
|
211
404
|
try {
|
|
212
|
-
await this.#
|
|
405
|
+
await this.#createDefinition(definition, signal);
|
|
213
406
|
}
|
|
214
407
|
catch (error) {
|
|
408
|
+
if (!(error instanceof AggregateError))
|
|
409
|
+
this.#forgetReader(id);
|
|
410
|
+
if (signal.aborted && !(error instanceof AggregateError))
|
|
411
|
+
throw new ConnectError("Reader operation cancelled.", Code.Canceled);
|
|
215
412
|
throw this.#availabilityError(error);
|
|
216
413
|
}
|
|
217
|
-
if (this.#isDraining()) {
|
|
218
|
-
await this.#cancelDefinition(
|
|
414
|
+
if (this.#isDraining() || signal.aborted) {
|
|
415
|
+
await this.#cancelDefinition(definition, new AbortController().signal);
|
|
219
416
|
throw new ConnectError("Managed application is closing.", Code.Unavailable);
|
|
220
417
|
}
|
|
221
418
|
this.#schedulePendingActivation(subscription);
|
|
222
419
|
return subscription;
|
|
223
420
|
}
|
|
421
|
+
/**
|
|
422
|
+
* Cancels a pending child creation when the requesting reader loses authority.
|
|
423
|
+
*
|
|
424
|
+
* @param definition Logical subscription definition.
|
|
425
|
+
* @param signal Reader operation lifetime.
|
|
426
|
+
* @returns Completion after creation or conditional cancellation settles.
|
|
427
|
+
*/
|
|
428
|
+
async #createDefinition(definition, signal) {
|
|
429
|
+
let cancellation;
|
|
430
|
+
const abort = () => {
|
|
431
|
+
cancellation = this.#cancelDefinition(definition, new AbortController().signal);
|
|
432
|
+
void cancellation.catch(() => undefined);
|
|
433
|
+
};
|
|
434
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
435
|
+
let failed = false;
|
|
436
|
+
let primary;
|
|
437
|
+
try {
|
|
438
|
+
await this.#subscriptionKernel.subscribe(definition, signal);
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
failed = true;
|
|
442
|
+
primary = error;
|
|
443
|
+
}
|
|
444
|
+
finally {
|
|
445
|
+
signal.removeEventListener("abort", abort);
|
|
446
|
+
}
|
|
447
|
+
if (signal.aborted && cancellation === undefined)
|
|
448
|
+
abort();
|
|
449
|
+
try {
|
|
450
|
+
await cancellation;
|
|
451
|
+
}
|
|
452
|
+
catch (cleanup) {
|
|
453
|
+
throw new AggregateError(failed ? [primary, cleanup] : [cleanup], "Subscription cancellation failed.");
|
|
454
|
+
}
|
|
455
|
+
if (failed)
|
|
456
|
+
throw primary;
|
|
457
|
+
if (signal.aborted)
|
|
458
|
+
throw new ConnectError("Reader operation cancelled.", Code.Canceled);
|
|
459
|
+
}
|
|
224
460
|
/**
|
|
225
461
|
* Reads current admission state after an asynchronous backend Subscribe.
|
|
226
462
|
*
|
|
@@ -229,7 +465,32 @@ export class NodeCoordinator {
|
|
|
229
465
|
#isDraining() {
|
|
230
466
|
return this.#draining;
|
|
231
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* Verifies membership and activates a retained subscription.
|
|
470
|
+
*
|
|
471
|
+
* @param subscription The subscription definition being processed.
|
|
472
|
+
* @param context The trusted context for this operation.
|
|
473
|
+
* @returns The update stream after admission.
|
|
474
|
+
*/
|
|
232
475
|
async *#activate(subscription, context) {
|
|
476
|
+
const operation = new NativeOperation(context.signal, this.#admissionShutdown.signal);
|
|
477
|
+
try {
|
|
478
|
+
await this.#admitActivation(subscription, context, operation);
|
|
479
|
+
operation.check();
|
|
480
|
+
yield* this.#relayActivation(subscription, operation);
|
|
481
|
+
}
|
|
482
|
+
finally {
|
|
483
|
+
operation.close();
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Relays one admitted definition through ACK and complete child iteration.
|
|
488
|
+
*
|
|
489
|
+
* @param subscription Retained logical definition.
|
|
490
|
+
* @param operation Host-managed activation lifetime.
|
|
491
|
+
* @returns ACK and surviving updates.
|
|
492
|
+
*/
|
|
493
|
+
async *#relayActivation(subscription, operation) {
|
|
233
494
|
if (this.#draining)
|
|
234
495
|
throw new ConnectError("Managed application is closing.", Code.Unavailable);
|
|
235
496
|
const definition = toBinary(SubscriptionSchema, subscription);
|
|
@@ -238,46 +499,84 @@ export class NodeCoordinator {
|
|
|
238
499
|
const abort = () => {
|
|
239
500
|
overflow.abort();
|
|
240
501
|
};
|
|
241
|
-
|
|
242
|
-
if (
|
|
502
|
+
operation.signal.addEventListener("abort", abort, { once: true });
|
|
503
|
+
if (operation.signal.aborted)
|
|
243
504
|
abort();
|
|
244
|
-
const updates = new SubscriptionUpdateQueue(
|
|
245
|
-
const
|
|
505
|
+
const updates = new SubscriptionUpdateQueue(this.#subscriptionQueueCapacity, abort);
|
|
506
|
+
const ack = Promise.withResolvers();
|
|
507
|
+
const activation = this.#activateChild(definition, subscription, updates, ack, overflow.signal);
|
|
246
508
|
let failure;
|
|
247
509
|
try {
|
|
248
|
-
|
|
510
|
+
const ready = await ack.promise;
|
|
511
|
+
operation.check();
|
|
512
|
+
if (ready !== undefined)
|
|
513
|
+
yield ready;
|
|
514
|
+
for await (const update of updates) {
|
|
515
|
+
operation.check();
|
|
249
516
|
yield update;
|
|
517
|
+
}
|
|
250
518
|
}
|
|
251
519
|
catch (error) {
|
|
252
520
|
failure = NodeCoordinatorValues.error(error);
|
|
253
521
|
}
|
|
254
522
|
finally {
|
|
255
|
-
|
|
523
|
+
operation.signal.removeEventListener("abort", abort);
|
|
256
524
|
overflow.abort();
|
|
257
525
|
updates.close();
|
|
258
526
|
await this.#finishActivation(definition, activation, failure);
|
|
259
527
|
}
|
|
260
528
|
}
|
|
529
|
+
/**
|
|
530
|
+
* Admits and validates a reader-bound activation before changing pending state.
|
|
531
|
+
*
|
|
532
|
+
* @param subscription The subscription definition being processed.
|
|
533
|
+
* @param context The trusted context for this operation.
|
|
534
|
+
* @returns Completion after the operation settles.
|
|
535
|
+
*/
|
|
536
|
+
async #admitActivation(subscription, context, operation) {
|
|
537
|
+
const admission = await this.#admit("spine.client.SubscriptionService", "Activate", toBinary(SubscriptionSchema, subscription), context, operation.signal);
|
|
538
|
+
operation.link(admission.invalidated);
|
|
539
|
+
operation.check();
|
|
540
|
+
ReadRequestStatus.check("subscription", () => Validate.check(SubscriptionSchema, subscription));
|
|
541
|
+
ReadRequestStatus.checkLifecycleCriterion(subscription);
|
|
542
|
+
this.#requireReader(subscription, admission.reader);
|
|
543
|
+
}
|
|
261
544
|
/**
|
|
262
545
|
* Relays accepted child updates through the bounded logical stream.
|
|
263
546
|
*
|
|
264
547
|
* @param definition The validated child subscription bytes.
|
|
265
548
|
* @param subscription The logical subscription exposed to callers.
|
|
266
549
|
* @param updates The bounded caller stream queue.
|
|
550
|
+
* @param ack The separate activation acknowledgment gate.
|
|
267
551
|
* @param signal Cancels the active child relay.
|
|
268
552
|
* @returns Completion after the backend activation ends.
|
|
269
553
|
*/
|
|
270
|
-
#activateChild(definition, subscription, updates, signal) {
|
|
554
|
+
#activateChild(definition, subscription, updates, ack, signal) {
|
|
271
555
|
return this.#subscriptionKernel
|
|
272
556
|
.activate(definition, async (childUpdate) => {
|
|
557
|
+
if (signal.aborted)
|
|
558
|
+
return;
|
|
273
559
|
const update = fromBinary(SubscriptionUpdateSchema, childUpdate);
|
|
274
560
|
update.subscription = clone(SubscriptionSchema, subscription);
|
|
561
|
+
if (update.response?.status?.status.case === "ok" && update.update.case === undefined) {
|
|
562
|
+
ack.resolve(update);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
275
565
|
await updates.push(update);
|
|
276
566
|
}, signal)
|
|
277
567
|
.finally(() => {
|
|
568
|
+
ack.resolve(undefined);
|
|
278
569
|
updates.close();
|
|
279
570
|
});
|
|
280
571
|
}
|
|
572
|
+
/**
|
|
573
|
+
* Finishes one activated stream and clears pending state.
|
|
574
|
+
*
|
|
575
|
+
* @param definition The serialized subscription definition.
|
|
576
|
+
* @param activation Activation task and its stream state.
|
|
577
|
+
* @param previousFailure Earlier lifecycle failure.
|
|
578
|
+
* @returns Completion after the operation settles.
|
|
579
|
+
*/
|
|
281
580
|
async #finishActivation(definition, activation, previousFailure) {
|
|
282
581
|
const failures = previousFailure === undefined ? [] : [previousFailure];
|
|
283
582
|
try {
|
|
@@ -298,16 +597,51 @@ export class NodeCoordinator {
|
|
|
298
597
|
if (failures.length > 1)
|
|
299
598
|
throw new AggregateError(failures, "Coordinator activation cleanup failed.");
|
|
300
599
|
}
|
|
600
|
+
/**
|
|
601
|
+
* Verifies reader membership and removes a retained subscription.
|
|
602
|
+
*
|
|
603
|
+
* @param subscription The subscription definition being processed.
|
|
604
|
+
* @param context The trusted context for this operation.
|
|
605
|
+
* @returns The cancellation acknowledgment.
|
|
606
|
+
*/
|
|
301
607
|
async #cancel(subscription, context) {
|
|
608
|
+
return this.#runAdmitted("spine.client.SubscriptionService", "Cancel", toBinary(SubscriptionSchema, subscription), context, (reader, signal) => this.#cancelReader(subscription, reader, signal));
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Checks reader binding before removing an existing definition.
|
|
612
|
+
*
|
|
613
|
+
* @param subscription Logical definition.
|
|
614
|
+
* @param reader Detached authenticated facts.
|
|
615
|
+
* @param signal Effective cancellation.
|
|
616
|
+
* @returns Cancellation acknowledgment.
|
|
617
|
+
*/
|
|
618
|
+
async #cancelReader(subscription, reader, signal) {
|
|
619
|
+
ReadRequestStatus.check("subscription", () => Validate.check(SubscriptionSchema, subscription));
|
|
620
|
+
ReadRequestStatus.checkLifecycleCriterion(subscription);
|
|
621
|
+
const id = subscription.id?.value;
|
|
622
|
+
if (id !== undefined && this.#readers.has(id))
|
|
623
|
+
this.#requireReader(subscription, reader);
|
|
624
|
+
else
|
|
625
|
+
return create(ResponseSchema, {
|
|
626
|
+
status: create(StatusSchema, { status: { case: "ok", value: create(EmptySchema) } }),
|
|
627
|
+
});
|
|
302
628
|
this.#clearPendingActivation(subscription);
|
|
303
|
-
await this.#cancelDefinition(toBinary(SubscriptionSchema, subscription),
|
|
629
|
+
await this.#cancelDefinition(toBinary(SubscriptionSchema, subscription), signal);
|
|
304
630
|
return create(ResponseSchema, {
|
|
305
631
|
status: create(StatusSchema, { status: { case: "ok", value: create(EmptySchema) } }),
|
|
306
632
|
});
|
|
307
633
|
}
|
|
634
|
+
/**
|
|
635
|
+
* Forwards a verified request to the selected managed child.
|
|
636
|
+
*
|
|
637
|
+
* @param request The request being processed.
|
|
638
|
+
* @returns Completion after the operation settles.
|
|
639
|
+
*/
|
|
308
640
|
async #forward(request) {
|
|
309
641
|
if (this.#draining)
|
|
310
642
|
throw new ConnectError("No ready application replica is available.", Code.Unavailable);
|
|
643
|
+
if (request.signal.aborted)
|
|
644
|
+
throw new ConnectError("Reader operation cancelled.", Code.Canceled);
|
|
311
645
|
try {
|
|
312
646
|
await this.#unaryKernel.forward(request);
|
|
313
647
|
}
|
|
@@ -315,11 +649,22 @@ export class NodeCoordinator {
|
|
|
315
649
|
throw this.#availabilityError(error);
|
|
316
650
|
}
|
|
317
651
|
}
|
|
652
|
+
/**
|
|
653
|
+
* Maps child transport failure to the public availability status.
|
|
654
|
+
*
|
|
655
|
+
* @param error The failure being classified or propagated.
|
|
656
|
+
* @returns The public transport error.
|
|
657
|
+
*/
|
|
318
658
|
#availabilityError(error) {
|
|
319
659
|
return error instanceof Error && error.message === "backend membership is unavailable."
|
|
320
660
|
? new ConnectError("No ready application replica is available.", Code.Unavailable)
|
|
321
661
|
: error;
|
|
322
662
|
}
|
|
663
|
+
/**
|
|
664
|
+
* Starts the activation handshake expiry for a new subscription.
|
|
665
|
+
*
|
|
666
|
+
* @param subscription The subscription definition being processed.
|
|
667
|
+
*/
|
|
323
668
|
#schedulePendingActivation(subscription) {
|
|
324
669
|
const id = NodeCoordinatorValues.requiredValue(subscription.id?.value, "Coordinator subscription definition is missing an ID.");
|
|
325
670
|
this.#clearPendingActivation(subscription);
|
|
@@ -332,18 +677,98 @@ export class NodeCoordinator {
|
|
|
332
677
|
pending.timer.unref();
|
|
333
678
|
this.#pendingActivations.set(id, pending);
|
|
334
679
|
}
|
|
680
|
+
/**
|
|
681
|
+
* Forwards cancellation of a native subscription definition.
|
|
682
|
+
*
|
|
683
|
+
* @param definition The serialized subscription definition.
|
|
684
|
+
* @param signal The cancellation signal for this operation.
|
|
685
|
+
* @returns Completion after the operation settles.
|
|
686
|
+
*/
|
|
335
687
|
#cancelDefinition(definition, signal) {
|
|
336
688
|
const id = NodeCoordinatorValues.requiredValue(fromBinary(SubscriptionSchema, definition).id?.value, "Coordinator subscription definition is missing an ID.");
|
|
337
689
|
const active = this.#cancellations.get(id);
|
|
338
690
|
if (active !== undefined)
|
|
339
691
|
return active;
|
|
340
|
-
const cancellation = this.#subscriptionKernel
|
|
692
|
+
const cancellation = this.#subscriptionKernel
|
|
693
|
+
.cancel(definition, signal)
|
|
694
|
+
.then(() => {
|
|
695
|
+
this.#forgetReader(id);
|
|
696
|
+
})
|
|
697
|
+
.finally(() => {
|
|
341
698
|
if (this.#cancellations.get(id) === cancellation)
|
|
342
699
|
this.#cancellations.delete(id);
|
|
343
700
|
});
|
|
344
701
|
this.#cancellations.set(id, cancellation);
|
|
345
702
|
return cancellation;
|
|
346
703
|
}
|
|
704
|
+
/**
|
|
705
|
+
* Verifies and refreshes the reader before subscription effects.
|
|
706
|
+
*
|
|
707
|
+
* @param subscription The subscription definition being processed.
|
|
708
|
+
* @param reader The verified reader facts for this operation.
|
|
709
|
+
*/
|
|
710
|
+
#requireReader(subscription, reader) {
|
|
711
|
+
const id = subscription.id?.value;
|
|
712
|
+
const previous = id === undefined ? undefined : this.#readers.get(id);
|
|
713
|
+
if (id === undefined || previous === undefined)
|
|
714
|
+
throw ReadRequestStatus.unknownSubscription();
|
|
715
|
+
if (!ReaderIdentities.matches(ReaderIdentities.from(previous), ReaderIdentities.from(reader)))
|
|
716
|
+
throw new ConnectError("Subscription reader is not authorized.", Code.PermissionDenied);
|
|
717
|
+
this.#readers.set(id, reader);
|
|
718
|
+
for (const child of this.#children.get(id) ?? [])
|
|
719
|
+
this.#readers.set(child, reader);
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Associates one native child with the admitted logical reader.
|
|
723
|
+
*
|
|
724
|
+
* @param definition The serialized subscription definition.
|
|
725
|
+
* @param child The native child subscription definition.
|
|
726
|
+
*/
|
|
727
|
+
#rememberChild(definition, child) {
|
|
728
|
+
const id = fromBinary(SubscriptionSchema, definition).id?.value;
|
|
729
|
+
const childId = fromBinary(SubscriptionSchema, child).id?.value;
|
|
730
|
+
if (id === undefined || childId === undefined)
|
|
731
|
+
return;
|
|
732
|
+
const logicalId = this.#logicalForChild.get(id) ?? (this.#readers.has(id) ? id : undefined);
|
|
733
|
+
if (logicalId === undefined)
|
|
734
|
+
return;
|
|
735
|
+
const children = this.#children.get(logicalId) ?? new Set();
|
|
736
|
+
children.add(childId);
|
|
737
|
+
this.#children.set(logicalId, children);
|
|
738
|
+
this.#logicalForChild.set(childId, logicalId);
|
|
739
|
+
const reader = this.#readers.get(id);
|
|
740
|
+
if (reader !== undefined)
|
|
741
|
+
this.#readers.set(childId, reader);
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Returns current detached facts for a logical or child subscription.
|
|
745
|
+
*
|
|
746
|
+
* @param bytes The serialized message bytes.
|
|
747
|
+
* @returns The verified reader for this subscription.
|
|
748
|
+
*/
|
|
749
|
+
#readerFor(bytes) {
|
|
750
|
+
const id = fromBinary(SubscriptionSchema, bytes).id?.value;
|
|
751
|
+
const reader = id === undefined ? undefined : this.#readers.get(id);
|
|
752
|
+
return reader === undefined ? undefined : snapshotTrustedReader(reader);
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Removes a finished logical definition and its current child mappings.
|
|
756
|
+
*
|
|
757
|
+
* @param id The stable identifier for this record.
|
|
758
|
+
*/
|
|
759
|
+
#forgetReader(id) {
|
|
760
|
+
this.#readers.delete(id);
|
|
761
|
+
for (const child of this.#children.get(id) ?? []) {
|
|
762
|
+
this.#readers.delete(child);
|
|
763
|
+
this.#logicalForChild.delete(child);
|
|
764
|
+
}
|
|
765
|
+
this.#children.delete(id);
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Removes a pending activation timer and its tracking entry.
|
|
769
|
+
*
|
|
770
|
+
* @param subscription The subscription definition being processed.
|
|
771
|
+
*/
|
|
347
772
|
#clearPendingActivation(subscription) {
|
|
348
773
|
const id = subscription.id?.value;
|
|
349
774
|
if (id === undefined)
|
|
@@ -354,6 +779,13 @@ export class NodeCoordinator {
|
|
|
354
779
|
clearTimeout(pending.timer);
|
|
355
780
|
this.#pendingActivations.delete(id);
|
|
356
781
|
}
|
|
782
|
+
/**
|
|
783
|
+
* Expires an unactivated subscription after its handshake window.
|
|
784
|
+
*
|
|
785
|
+
* @param id The stable identifier for this record.
|
|
786
|
+
* @param pending Pending activation or retry state.
|
|
787
|
+
* @returns Completion after the operation settles.
|
|
788
|
+
*/
|
|
357
789
|
async #expirePendingActivation(id, pending) {
|
|
358
790
|
if (this.#pendingActivations.get(id) !== pending)
|
|
359
791
|
return;
|
|
@@ -367,6 +799,11 @@ export class NodeCoordinator {
|
|
|
367
799
|
this.#schedulePendingActivation(fromBinary(SubscriptionSchema, pending.definition));
|
|
368
800
|
}
|
|
369
801
|
}
|
|
802
|
+
/**
|
|
803
|
+
* Reconciles managed child subscriptions after topology changes.
|
|
804
|
+
*
|
|
805
|
+
* @returns Completion after the operation settles.
|
|
806
|
+
*/
|
|
370
807
|
#reconcile() {
|
|
371
808
|
if (this.#members.onRelaySynchronized !== undefined)
|
|
372
809
|
return this.#subscriptionKernel
|
|
@@ -377,6 +814,11 @@ export class NodeCoordinator {
|
|
|
377
814
|
this.#subscriptionKernel.reconcile(this.#members.relayMembers()),
|
|
378
815
|
]).then(() => undefined);
|
|
379
816
|
}
|
|
817
|
+
/**
|
|
818
|
+
* Runs shutdown once while concurrent callers await the same completion.
|
|
819
|
+
*
|
|
820
|
+
* @returns Completion after the operation settles.
|
|
821
|
+
*/
|
|
380
822
|
async #closeOnce() {
|
|
381
823
|
this.#stopMembers.forEach((stop) => {
|
|
382
824
|
stop();
|
|
@@ -395,16 +837,16 @@ export class NodeCoordinator {
|
|
|
395
837
|
* @internal
|
|
396
838
|
*/
|
|
397
839
|
export class SubscriptionUpdateQueue {
|
|
840
|
+
#active;
|
|
398
841
|
#updates = [];
|
|
399
842
|
#waiters = [];
|
|
400
|
-
#delivered = [];
|
|
401
843
|
#limit;
|
|
402
844
|
#onOverflow;
|
|
403
845
|
#closed = false;
|
|
404
846
|
/**
|
|
405
847
|
* Creates one terminal bounded update queue.
|
|
406
848
|
*
|
|
407
|
-
* @param limit
|
|
849
|
+
* @param limit Maximum waiting updates beyond one active update.
|
|
408
850
|
* @param onOverflow Aborts the public relay when the queue reaches its bound.
|
|
409
851
|
*/
|
|
410
852
|
constructor(limit, onOverflow = () => undefined) {
|
|
@@ -420,18 +862,22 @@ export class SubscriptionUpdateQueue {
|
|
|
420
862
|
push(update) {
|
|
421
863
|
if (this.#closed)
|
|
422
864
|
return Promise.resolve();
|
|
423
|
-
|
|
424
|
-
if (waiter !== undefined) {
|
|
425
|
-
waiter({ value: update, done: false });
|
|
426
|
-
return Promise.resolve();
|
|
427
|
-
}
|
|
428
|
-
if (this.#updates.length >= this.#limit) {
|
|
865
|
+
if (this.#active !== undefined && this.#updates.length >= this.#limit) {
|
|
429
866
|
this.#onOverflow();
|
|
430
867
|
this.close();
|
|
431
868
|
return Promise.resolve();
|
|
432
869
|
}
|
|
433
|
-
|
|
434
|
-
|
|
870
|
+
return new Promise((resolve) => {
|
|
871
|
+
const entry = { update, delivered: false, resolve };
|
|
872
|
+
if (this.#active === undefined) {
|
|
873
|
+
this.#active = entry;
|
|
874
|
+
const waiter = this.#waiters.shift();
|
|
875
|
+
if (waiter !== undefined)
|
|
876
|
+
waiter(this.#deliver(entry));
|
|
877
|
+
}
|
|
878
|
+
else
|
|
879
|
+
this.#updates.push(entry);
|
|
880
|
+
});
|
|
435
881
|
}
|
|
436
882
|
/**
|
|
437
883
|
* Closes the queue and releases all waiting producers and consumers.
|
|
@@ -440,11 +886,20 @@ export class SubscriptionUpdateQueue {
|
|
|
440
886
|
if (this.#closed)
|
|
441
887
|
return;
|
|
442
888
|
this.#closed = true;
|
|
443
|
-
this.#
|
|
889
|
+
this.#active?.resolve();
|
|
890
|
+
this.#active = undefined;
|
|
891
|
+
for (const update of this.#updates.splice(0))
|
|
892
|
+
update.resolve();
|
|
444
893
|
for (const waiter of this.#waiters.splice(0))
|
|
445
894
|
waiter({ value: undefined, done: true });
|
|
446
|
-
|
|
447
|
-
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Resolves one waiting consumer with the next queued subscription update.
|
|
898
|
+
*/
|
|
899
|
+
#deliver(entry) {
|
|
900
|
+
entry.delivered = true;
|
|
901
|
+
entry.resolve();
|
|
902
|
+
return { value: entry.update, done: false };
|
|
448
903
|
}
|
|
449
904
|
/**
|
|
450
905
|
* Returns queued updates until the terminal close.
|
|
@@ -454,13 +909,12 @@ export class SubscriptionUpdateQueue {
|
|
|
454
909
|
[Symbol.asyncIterator]() {
|
|
455
910
|
return {
|
|
456
911
|
next: () => {
|
|
457
|
-
const update = this.#updates.shift();
|
|
458
|
-
if (update !== undefined) {
|
|
459
|
-
this.#delivered.shift()?.();
|
|
460
|
-
return Promise.resolve({ value: update, done: false });
|
|
461
|
-
}
|
|
462
912
|
if (this.#closed)
|
|
463
913
|
return Promise.resolve({ value: undefined, done: true });
|
|
914
|
+
if (this.#active?.delivered)
|
|
915
|
+
this.#active = this.#updates.shift();
|
|
916
|
+
if (this.#active !== undefined)
|
|
917
|
+
return Promise.resolve(this.#deliver(this.#active));
|
|
464
918
|
return new Promise((resolve) => this.#waiters.push(resolve));
|
|
465
919
|
},
|
|
466
920
|
};
|
|
@@ -472,20 +926,35 @@ const NodeCoordinatorValues = Object.freeze({
|
|
|
472
926
|
*
|
|
473
927
|
* @param members The ready member source when available.
|
|
474
928
|
* @returns The configured membership kernel options.
|
|
929
|
+
* @param issuer The configured forwarding proof producer.
|
|
930
|
+
* @param maxBytes The existing native message-size bound.
|
|
931
|
+
* @param readerFor Reader lookup for a retained subscription.
|
|
932
|
+
* @param onRememberChild Records a native child ID for its logical subscription.
|
|
475
933
|
*/
|
|
476
|
-
unaryKernelOptions(members) {
|
|
477
|
-
// The Coordinator deliberately uses only the kernel's member lifecycle and
|
|
478
|
-
// one-shot forwarding operations. Subscription fan-out is owned by T-0208.
|
|
934
|
+
unaryKernelOptions(members, issuer, maxBytes = defaultMessageMaxBytes, readerFor, onRememberChild) {
|
|
479
935
|
return {
|
|
480
|
-
create: (member) => Promise.resolve(NodeCoordinatorValues.client(member, members)),
|
|
936
|
+
create: (member) => Promise.resolve(NodeCoordinatorValues.client(member, members, issuer, maxBytes, readerFor, onRememberChild)),
|
|
481
937
|
memberKey: (member) => `${member.slot.toString()}/${member.incarnation}`,
|
|
482
938
|
sameMember: (left, right) => left.endpoint === right.endpoint,
|
|
483
939
|
definitionKey: (definition) => fromBinary(SubscriptionSchema, definition).id?.value,
|
|
484
|
-
childDefinition: NodeCoordinatorValues.
|
|
940
|
+
childDefinition: (definition, member) => NodeCoordinatorValues.rememberChildDefinition(definition, member, onRememberChild),
|
|
485
941
|
childSize: (child) => child.byteLength,
|
|
486
942
|
...NodeCoordinatorValues.activationAckOptions(members),
|
|
487
943
|
};
|
|
488
944
|
},
|
|
945
|
+
/**
|
|
946
|
+
* Builds and records a child definition before member subscription.
|
|
947
|
+
*
|
|
948
|
+
* @param definition The serialized subscription definition.
|
|
949
|
+
* @param member The selected backend member.
|
|
950
|
+
* @param onRememberChild Records a native child ID for its logical subscription.
|
|
951
|
+
* @returns The retained managed-child definition.
|
|
952
|
+
*/
|
|
953
|
+
rememberChildDefinition(definition, member, onRememberChild) {
|
|
954
|
+
const child = NodeCoordinatorValues.childDefinition(definition, member);
|
|
955
|
+
onRememberChild?.(definition, child);
|
|
956
|
+
return child;
|
|
957
|
+
},
|
|
489
958
|
/**
|
|
490
959
|
* Builds strict child ACK validation for the subscription membership kernel.
|
|
491
960
|
*
|
|
@@ -526,49 +995,178 @@ const NodeCoordinatorValues = Object.freeze({
|
|
|
526
995
|
* @param member The discovered backend member.
|
|
527
996
|
* @param members The ready member source when available.
|
|
528
997
|
* @returns The member transport client.
|
|
998
|
+
* @param issuer The configured forwarding proof producer.
|
|
999
|
+
* @param maxBytes The existing native message-size bound.
|
|
1000
|
+
* @param readerFor Reader lookup for a retained subscription.
|
|
1001
|
+
* @param onRememberChild Records a native child ID for its logical subscription.
|
|
529
1002
|
*/
|
|
530
|
-
client(member, members) {
|
|
1003
|
+
client(member, members, issuer, maxBytes = defaultMessageMaxBytes, readerFor, onRememberChild) {
|
|
531
1004
|
const manager = new Http2SessionManager(member.endpoint);
|
|
532
1005
|
const transport = createGrpcTransport({ baseUrl: member.endpoint, sessionManager: manager });
|
|
533
1006
|
return {
|
|
534
|
-
forward: (request) => NodeCoordinatorValues.forward(transport, request),
|
|
535
|
-
close: () =>
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
},
|
|
539
|
-
subscribe: async (definition, signal) => {
|
|
540
|
-
const subscription = fromBinary(SubscriptionSchema, definition);
|
|
541
|
-
const created = await createClient(SubscriptionService, transport).subscribe(NodeCoordinatorValues.requiredValue(subscription.topic, "Coordinator subscription definition is missing a Topic."), { signal });
|
|
542
|
-
return toBinary(SubscriptionSchema, created);
|
|
543
|
-
},
|
|
544
|
-
activate: (child, updates, signal) => NodeCoordinatorValues.activateChild(transport, member, members, child, updates, signal),
|
|
545
|
-
dispose: async (child, signal) => {
|
|
546
|
-
const subscription = fromBinary(SubscriptionSchema, child);
|
|
547
|
-
members?.onChildSubscriptionCancelled?.(member, subscription);
|
|
548
|
-
await createClient(SubscriptionService, transport).cancel(subscription, { signal });
|
|
549
|
-
},
|
|
1007
|
+
forward: (request) => NodeCoordinatorValues.forward(transport, request, issuer, maxBytes),
|
|
1008
|
+
close: () => NodeCoordinatorValues.closeManager(manager),
|
|
1009
|
+
subscribe: (definition, signal) => NodeCoordinatorValues.subscribeChild(transport, definition, signal, readerFor?.(definition), issuer, maxBytes, onRememberChild),
|
|
1010
|
+
...NodeCoordinatorValues.childOperations(transport, member, members, issuer, maxBytes, readerFor),
|
|
550
1011
|
};
|
|
551
1012
|
},
|
|
552
1013
|
/**
|
|
553
|
-
*
|
|
1014
|
+
* Closes the transport manager for a departed native member.
|
|
1015
|
+
*
|
|
1016
|
+
* @param manager Native session manager.
|
|
1017
|
+
* @returns Completion after the operation settles.
|
|
1018
|
+
*/
|
|
1019
|
+
closeManager(manager) {
|
|
1020
|
+
manager.abort();
|
|
1021
|
+
return Promise.resolve();
|
|
1022
|
+
},
|
|
1023
|
+
/**
|
|
1024
|
+
* Returns streaming and cancellation operations for a native member.
|
|
1025
|
+
*
|
|
1026
|
+
* @param transport The connected native transport.
|
|
1027
|
+
* @param member The selected backend member.
|
|
1028
|
+
* @param members The available backend member source.
|
|
1029
|
+
* @param issuer The configured forwarding proof producer.
|
|
1030
|
+
* @param maxBytes The existing native message-size bound.
|
|
1031
|
+
* @param readerFor Reader lookup for a retained subscription.
|
|
1032
|
+
* @returns The managed-child operations.
|
|
1033
|
+
*/
|
|
1034
|
+
childOperations(transport, member, members, issuer, maxBytes, readerFor) {
|
|
1035
|
+
return {
|
|
1036
|
+
activate: NodeCoordinatorValues.activation(transport, member, members, issuer, maxBytes, readerFor),
|
|
1037
|
+
dispose: (child, signal) => NodeCoordinatorValues.cancelChild(transport, member, members, child, signal, readerFor?.(child), issuer, maxBytes),
|
|
1038
|
+
};
|
|
1039
|
+
},
|
|
1040
|
+
/**
|
|
1041
|
+
* Captures native member context for one child stream.
|
|
1042
|
+
*
|
|
1043
|
+
* @param transport The connected native transport.
|
|
1044
|
+
* @param member The selected backend member.
|
|
1045
|
+
* @param members The available backend member source.
|
|
1046
|
+
* @param issuer The configured forwarding proof producer.
|
|
1047
|
+
* @param maxBytes The existing native message-size bound.
|
|
1048
|
+
* @param readerFor Reader lookup for a retained subscription.
|
|
1049
|
+
* @returns The activation stream completion.
|
|
1050
|
+
*/
|
|
1051
|
+
activation(transport, member, members, issuer, maxBytes, readerFor) {
|
|
1052
|
+
return (child, updates, signal) => NodeCoordinatorValues.activateChild(transport, member, members, child, updates, signal, readerFor?.(child), issuer, maxBytes);
|
|
1053
|
+
},
|
|
1054
|
+
/**
|
|
1055
|
+
* Creates one native child and records any backend-assigned ID.
|
|
1056
|
+
*
|
|
1057
|
+
* @param transport The connected native transport.
|
|
1058
|
+
* @param definition The serialized subscription definition.
|
|
1059
|
+
* @param signal The cancellation signal for this operation.
|
|
1060
|
+
* @param reader The verified reader facts for this operation.
|
|
1061
|
+
* @param issuer The configured forwarding proof producer.
|
|
1062
|
+
* @param maxBytes The existing native message-size bound.
|
|
1063
|
+
* @param onRememberChild Records a native child ID for its logical subscription.
|
|
1064
|
+
* @returns The managed-child subscription.
|
|
1065
|
+
*/
|
|
1066
|
+
async subscribeChild(transport, definition, signal, reader, issuer, maxBytes, onRememberChild) {
|
|
1067
|
+
const subscription = fromBinary(SubscriptionSchema, definition);
|
|
1068
|
+
const topic = NodeCoordinatorValues.requiredValue(subscription.topic, "Coordinator subscription definition is missing a Topic.");
|
|
1069
|
+
const headers = await NodeCoordinatorValues.readerHeaders("Subscribe", toBinary(TopicSchema, topic), reader, issuer, maxBytes);
|
|
1070
|
+
if (signal.aborted)
|
|
1071
|
+
throw new ConnectError("Reader operation cancelled.", Code.Canceled);
|
|
1072
|
+
const created = await createClient(SubscriptionService, transport).subscribe(topic, {
|
|
1073
|
+
signal,
|
|
1074
|
+
...(headers === undefined ? {} : { headers }),
|
|
1075
|
+
});
|
|
1076
|
+
const bytes = toBinary(SubscriptionSchema, created);
|
|
1077
|
+
onRememberChild?.(definition, bytes);
|
|
1078
|
+
return bytes;
|
|
1079
|
+
},
|
|
1080
|
+
/**
|
|
1081
|
+
* Cancels one native child with the same verified reader as its creation.
|
|
1082
|
+
*
|
|
1083
|
+
* @param transport The connected native transport.
|
|
1084
|
+
* @param member The selected backend member.
|
|
1085
|
+
* @param members The available backend member source.
|
|
1086
|
+
* @param child The native child subscription definition.
|
|
1087
|
+
* @param signal The cancellation signal for this operation.
|
|
1088
|
+
* @param reader The verified reader facts for this operation.
|
|
1089
|
+
* @param issuer The configured forwarding proof producer.
|
|
1090
|
+
* @param maxBytes The existing native message-size bound.
|
|
1091
|
+
* @returns Completion after the operation settles.
|
|
1092
|
+
*/
|
|
1093
|
+
async cancelChild(transport, member, members, child, signal, reader, issuer, maxBytes) {
|
|
1094
|
+
const subscription = fromBinary(SubscriptionSchema, child);
|
|
1095
|
+
const headers = await NodeCoordinatorValues.readerHeaders("Cancel", child, reader, issuer, maxBytes);
|
|
1096
|
+
if (signal.aborted)
|
|
1097
|
+
throw new ConnectError("Reader operation cancelled.", Code.Canceled);
|
|
1098
|
+
members?.onChildSubscriptionCancelled?.(member, subscription);
|
|
1099
|
+
await createClient(SubscriptionService, transport).cancel(subscription, {
|
|
1100
|
+
signal,
|
|
1101
|
+
...(headers === undefined ? {} : { headers }),
|
|
1102
|
+
});
|
|
1103
|
+
},
|
|
1104
|
+
/**
|
|
1105
|
+
* Sends one native Command, entity read, or App Query to a ready member.
|
|
554
1106
|
*
|
|
555
1107
|
* @param transport The connected native transport.
|
|
556
1108
|
* @param request The request to forward.
|
|
557
1109
|
* @returns Empty routing bytes after the response is assigned.
|
|
1110
|
+
* @param issuer The configured forwarding proof producer.
|
|
1111
|
+
* @param maxBytes The existing native message-size bound.
|
|
558
1112
|
*/
|
|
559
|
-
async forward(transport, request) {
|
|
1113
|
+
async forward(transport, request, issuer, maxBytes = defaultMessageMaxBytes) {
|
|
1114
|
+
const headers = await NodeCoordinatorValues.forwardHeaders(request, issuer, maxBytes);
|
|
1115
|
+
if (request.signal.aborted)
|
|
1116
|
+
throw new ConnectError("Reader operation cancelled.", Code.Canceled);
|
|
560
1117
|
if (request.kind === "command") {
|
|
561
|
-
|
|
1118
|
+
await NodeCoordinatorValues.forwardCommand(transport, request, headers);
|
|
1119
|
+
}
|
|
1120
|
+
else if (request.kind === "query") {
|
|
1121
|
+
const response = await transport.unary(QueryService.method.read, request.signal, request.context.timeoutMs(), headers, request.query);
|
|
562
1122
|
NodeCoordinatorValues.responseMetadata(response.header, response.trailer, request.context);
|
|
563
1123
|
request.response = response.message;
|
|
564
1124
|
}
|
|
565
1125
|
else {
|
|
566
|
-
const response = await transport.unary(QueryService.method.
|
|
1126
|
+
const response = await transport.unary(QueryService.method.execute, request.signal, request.context.timeoutMs(), headers, request.query);
|
|
567
1127
|
NodeCoordinatorValues.responseMetadata(response.header, response.trailer, request.context);
|
|
568
1128
|
request.response = response.message;
|
|
569
1129
|
}
|
|
570
1130
|
return new Uint8Array();
|
|
571
1131
|
},
|
|
1132
|
+
/**
|
|
1133
|
+
* Invokes the Command branch and retains its native response metadata.
|
|
1134
|
+
*
|
|
1135
|
+
* @param transport The selected native child transport.
|
|
1136
|
+
* @param request The admitted Command and response slot.
|
|
1137
|
+
* @param headers Operation-bound verified-reader metadata.
|
|
1138
|
+
* @returns Completion after assigning the native response.
|
|
1139
|
+
*/
|
|
1140
|
+
async forwardCommand(transport, request, headers) {
|
|
1141
|
+
const response = await transport.unary(CommandService.method.post, request.signal, request.context.timeoutMs(), headers, request.command);
|
|
1142
|
+
NodeCoordinatorValues.responseMetadata(response.header, response.trailer, request.context);
|
|
1143
|
+
request.response = response.message;
|
|
1144
|
+
},
|
|
1145
|
+
/**
|
|
1146
|
+
* Encodes verified reader facts into one exact native unary operation.
|
|
1147
|
+
*
|
|
1148
|
+
* @param request The request being processed.
|
|
1149
|
+
* @param issuer The configured forwarding proof producer.
|
|
1150
|
+
* @param maxBytes The existing native message-size bound.
|
|
1151
|
+
* @returns The operation-bound forwarding headers.
|
|
1152
|
+
*/
|
|
1153
|
+
async forwardHeaders(request, issuer, maxBytes) {
|
|
1154
|
+
const service = request.kind === "command" ? "spine.client.CommandService" : "spine.client.QueryService";
|
|
1155
|
+
const method = request.kind === "command" ? "Post" : request.kind === "query" ? "Read" : "Execute";
|
|
1156
|
+
const value = request.kind === "command"
|
|
1157
|
+
? toBinary(CommandService.method.post.input, request.command)
|
|
1158
|
+
: request.kind === "query"
|
|
1159
|
+
? toBinary(QuerySchema, request.query)
|
|
1160
|
+
: toBinary(AppQuerySchema, request.query);
|
|
1161
|
+
const headers = NodeCoordinatorValues.applicationHeaders(request.context.requestHeader);
|
|
1162
|
+
if (issuer === undefined &&
|
|
1163
|
+
(request.reader.principal !== undefined || request.reader.authorityFacts !== undefined))
|
|
1164
|
+
throw new ConnectError("Managed reader forwarding is unavailable.", Code.Internal);
|
|
1165
|
+
if (issuer !== undefined)
|
|
1166
|
+
for (const [name, entry] of Object.entries(await ReaderForwarding.seal(service, method, value, request.reader, issuer, maxBytes)))
|
|
1167
|
+
headers.set(name, entry);
|
|
1168
|
+
return headers;
|
|
1169
|
+
},
|
|
572
1170
|
/**
|
|
573
1171
|
* Streams validated updates from one native child subscription.
|
|
574
1172
|
*
|
|
@@ -579,25 +1177,61 @@ const NodeCoordinatorValues = Object.freeze({
|
|
|
579
1177
|
* @param updates The logical update sink.
|
|
580
1178
|
* @param signal Cancels child activation.
|
|
581
1179
|
* @returns Completion after the child stream closes.
|
|
1180
|
+
* @param reader The verified reader facts for this operation.
|
|
1181
|
+
* @param issuer The configured forwarding proof producer.
|
|
1182
|
+
* @param maxBytes The existing native message-size bound.
|
|
582
1183
|
*/
|
|
583
|
-
async activateChild(transport, member, members, child, updates, signal) {
|
|
1184
|
+
async activateChild(transport, member, members, child, updates, signal, reader, issuer, maxBytes = defaultMessageMaxBytes) {
|
|
584
1185
|
const subscription = fromBinary(SubscriptionSchema, child);
|
|
1186
|
+
const headers = await NodeCoordinatorValues.readerHeaders("Activate", child, reader, issuer, maxBytes);
|
|
1187
|
+
if (signal.aborted)
|
|
1188
|
+
throw new ConnectError("Reader operation cancelled.", Code.Canceled);
|
|
585
1189
|
members?.onChildSubscriptionActivated?.(member, subscription);
|
|
586
1190
|
for await (const update of createClient(SubscriptionService, transport).activate(subscription, {
|
|
587
1191
|
signal,
|
|
1192
|
+
...(headers === undefined ? {} : { headers }),
|
|
588
1193
|
})) {
|
|
589
|
-
|
|
590
|
-
const updateTopic = update.subscription?.topic;
|
|
591
|
-
if (update.subscription?.id?.value !== subscription.id?.value ||
|
|
592
|
-
topic === undefined ||
|
|
593
|
-
updateTopic === undefined ||
|
|
594
|
-
!equals(TopicSchema, updateTopic, topic))
|
|
595
|
-
throw new Error("Backend child update does not match its subscription.");
|
|
1194
|
+
NodeCoordinatorValues.checkChildUpdate(subscription, update);
|
|
596
1195
|
await updates(toBinary(SubscriptionUpdateSchema, update));
|
|
597
1196
|
}
|
|
598
1197
|
},
|
|
599
1198
|
/**
|
|
600
|
-
*
|
|
1199
|
+
* Rejects a native update for a different child ID or Topic.
|
|
1200
|
+
*
|
|
1201
|
+
* @param subscription The subscription definition being processed.
|
|
1202
|
+
* @param update Native subscription update.
|
|
1203
|
+
*/
|
|
1204
|
+
checkChildUpdate(subscription, update) {
|
|
1205
|
+
const topic = subscription.topic;
|
|
1206
|
+
const updateTopic = update.subscription?.topic;
|
|
1207
|
+
if (update.subscription?.id?.value !== subscription.id?.value ||
|
|
1208
|
+
topic === undefined ||
|
|
1209
|
+
updateTopic === undefined ||
|
|
1210
|
+
!equals(TopicSchema, updateTopic, topic))
|
|
1211
|
+
throw new Error("Backend child update does not match its subscription.");
|
|
1212
|
+
},
|
|
1213
|
+
/**
|
|
1214
|
+
* Encodes one verified reader against the exact child subscription RPC.
|
|
1215
|
+
*
|
|
1216
|
+
* @param method The canonical RPC method name.
|
|
1217
|
+
* @param value Canonical child subscription RPC bytes.
|
|
1218
|
+
* @param reader The verified reader facts for this operation.
|
|
1219
|
+
* @param issuer The configured forwarding proof producer.
|
|
1220
|
+
* @param maxBytes The existing native message-size bound.
|
|
1221
|
+
* @returns The canonical reader assertion headers.
|
|
1222
|
+
*/
|
|
1223
|
+
async readerHeaders(method, value, reader, issuer, maxBytes) {
|
|
1224
|
+
if (reader === undefined)
|
|
1225
|
+
return undefined;
|
|
1226
|
+
if (issuer === undefined) {
|
|
1227
|
+
if (reader.principal !== undefined || reader.authorityFacts !== undefined)
|
|
1228
|
+
throw new ConnectError("Managed reader forwarding is unavailable.", Code.Internal);
|
|
1229
|
+
return undefined;
|
|
1230
|
+
}
|
|
1231
|
+
return new Headers(await ReaderForwarding.seal("spine.client.SubscriptionService", method, value, reader, issuer, maxBytes));
|
|
1232
|
+
},
|
|
1233
|
+
/**
|
|
1234
|
+
* Snapshots backend response metadata into the public context.
|
|
601
1235
|
*
|
|
602
1236
|
* @param headers Headers received from the backend.
|
|
603
1237
|
* @param trailers Trailers received from the backend.
|
|
@@ -610,7 +1244,7 @@ const NodeCoordinatorValues = Object.freeze({
|
|
|
610
1244
|
context.responseTrailer.append(name, value);
|
|
611
1245
|
},
|
|
612
1246
|
/**
|
|
613
|
-
*
|
|
1247
|
+
* Snapshots application headers for forwarding.
|
|
614
1248
|
*
|
|
615
1249
|
* @param headers Headers received from the backend.
|
|
616
1250
|
* @returns Forwardable application headers.
|
|
@@ -622,7 +1256,9 @@ const NodeCoordinatorValues = Object.freeze({
|
|
|
622
1256
|
name === "te" ||
|
|
623
1257
|
name === "user-agent" ||
|
|
624
1258
|
name.startsWith("grpc-") ||
|
|
625
|
-
name.startsWith("connect-")
|
|
1259
|
+
name.startsWith("connect-") ||
|
|
1260
|
+
name === "spine-internal-reader" ||
|
|
1261
|
+
name === "spine-internal-reader-proof")
|
|
626
1262
|
forwarded.delete(name);
|
|
627
1263
|
}
|
|
628
1264
|
return forwarded;
|