@spine-event-engine/server 2.0.0-snapshot.24 → 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 +86 -19
- package/REFERENCE.md +538 -150
- package/dist/agent/agent-acceptance-receipts.d.ts +69 -0
- package/dist/agent/agent-acceptance-receipts.d.ts.map +1 -0
- package/dist/agent/agent-acceptance-receipts.js +198 -0
- package/dist/agent/agent-acceptance-receipts.js.map +1 -0
- package/dist/agent/agent-execution-capacity.d.ts +37 -9
- package/dist/agent/agent-execution-capacity.d.ts.map +1 -1
- package/dist/agent/agent-execution-capacity.js +73 -15
- package/dist/agent/agent-execution-capacity.js.map +1 -1
- package/dist/agent/agent-history.d.ts +33 -12
- package/dist/agent/agent-history.d.ts.map +1 -1
- package/dist/agent/agent-history.js +61 -18
- package/dist/agent/agent-history.js.map +1 -1
- package/dist/agent/agent-live-ai-audit.d.ts +95 -0
- package/dist/agent/agent-live-ai-audit.d.ts.map +1 -0
- package/dist/agent/agent-live-ai-audit.js +290 -0
- package/dist/agent/agent-live-ai-audit.js.map +1 -0
- package/dist/agent/agent-live-ai-runtime.d.ts +300 -0
- package/dist/agent/agent-live-ai-runtime.d.ts.map +1 -0
- package/dist/agent/agent-live-ai-runtime.js +611 -0
- package/dist/agent/agent-live-ai-runtime.js.map +1 -0
- package/dist/agent/agent-live-audit.d.ts +67 -0
- package/dist/agent/agent-live-audit.d.ts.map +1 -0
- package/dist/agent/agent-live-audit.js +121 -0
- package/dist/agent/agent-live-audit.js.map +1 -0
- package/dist/agent/agent-live-budget.d.ts +84 -0
- package/dist/agent/agent-live-budget.d.ts.map +1 -0
- package/dist/agent/agent-live-budget.js +161 -0
- package/dist/agent/agent-live-budget.js.map +1 -0
- package/dist/agent/agent-live-fault.d.ts +18 -0
- package/dist/agent/agent-live-fault.d.ts.map +1 -0
- package/dist/agent/{agent-execution-fault.js → agent-live-fault.js} +7 -7
- package/dist/agent/agent-live-fault.js.map +1 -0
- package/dist/agent/agent-live-inputs.d.ts +74 -0
- package/dist/agent/agent-live-inputs.d.ts.map +1 -0
- package/dist/agent/agent-live-inputs.js +15 -0
- package/dist/agent/agent-live-inputs.js.map +1 -0
- package/dist/agent/agent-live-interaction-events.d.ts +46 -0
- package/dist/agent/agent-live-interaction-events.d.ts.map +1 -0
- package/dist/agent/agent-live-interaction-events.js +104 -0
- package/dist/agent/agent-live-interaction-events.js.map +1 -0
- package/dist/agent/agent-live-mcp-controls.d.ts +129 -0
- package/dist/agent/agent-live-mcp-controls.d.ts.map +1 -0
- package/dist/agent/agent-live-mcp-controls.js +228 -0
- package/dist/agent/agent-live-mcp-controls.js.map +1 -0
- package/dist/agent/agent-live-mcp-host.d.ts +88 -0
- package/dist/agent/agent-live-mcp-host.d.ts.map +1 -0
- package/dist/agent/{agent-mcp-host.js → agent-live-mcp-host.js} +1 -1
- package/dist/agent/agent-live-mcp-host.js.map +1 -0
- package/dist/agent/{agent-mcp-runtime.d.ts → agent-live-mcp-runtime.d.ts} +6 -6
- package/dist/agent/agent-live-mcp-runtime.d.ts.map +1 -0
- package/dist/agent/{agent-mcp-runtime.js → agent-live-mcp-runtime.js} +13 -42
- package/dist/agent/agent-live-mcp-runtime.js.map +1 -0
- package/dist/agent/agent-live-read-runtime.d.ts +72 -0
- package/dist/agent/agent-live-read-runtime.d.ts.map +1 -0
- package/dist/agent/agent-live-read-runtime.js +122 -0
- package/dist/agent/agent-live-read-runtime.js.map +1 -0
- package/dist/agent/agent-model-selection.d.ts +21 -34
- package/dist/agent/agent-model-selection.d.ts.map +1 -1
- package/dist/agent/agent-model-selection.js +61 -78
- package/dist/agent/agent-model-selection.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 +17 -18
- package/dist/bus/command-bus.d.ts.map +1 -1
- package/dist/bus/command-bus.js +97 -54
- 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 +20 -4
- package/dist/bus/event-bus.d.ts.map +1 -1
- package/dist/bus/event-bus.js +166 -14
- package/dist/bus/event-bus.js.map +1 -1
- package/dist/context/bounded-context.d.ts +169 -98
- package/dist/context/bounded-context.d.ts.map +1 -1
- package/dist/context/bounded-context.js +837 -503
- package/dist/context/bounded-context.js.map +1 -1
- package/dist/context/entity-inbox.d.ts +7 -2
- package/dist/context/entity-inbox.d.ts.map +1 -1
- package/dist/context/entity-inbox.js +22 -13
- package/dist/context/entity-inbox.js.map +1 -1
- package/dist/context/local-inbox-handoff.d.ts +34 -34
- package/dist/context/local-inbox-handoff.d.ts.map +1 -1
- package/dist/context/local-inbox-handoff.js +108 -150
- package/dist/context/local-inbox-handoff.js.map +1 -1
- package/dist/context/projection-handoff.d.ts +2 -2
- package/dist/context/projection-handoff.d.ts.map +1 -1
- package/dist/context/projection-handoff.js +10 -8
- package/dist/context/projection-handoff.js.map +1 -1
- package/dist/context/tenant-index.d.ts +36 -31
- package/dist/context/tenant-index.d.ts.map +1 -1
- package/dist/context/tenant-index.js +123 -125
- package/dist/context/tenant-index.js.map +1 -1
- package/dist/context/tenant-unknown-error.d.ts +33 -0
- package/dist/context/tenant-unknown-error.d.ts.map +1 -0
- package/dist/context/tenant-unknown-error.js +75 -0
- package/dist/context/tenant-unknown-error.js.map +1 -0
- package/dist/delivery/agent-delivery-scope.d.ts +66 -0
- package/dist/delivery/agent-delivery-scope.d.ts.map +1 -0
- package/dist/delivery/agent-delivery-scope.js +44 -0
- package/dist/delivery/agent-delivery-scope.js.map +1 -0
- package/dist/delivery/delivery-builder.d.ts +25 -1
- package/dist/delivery/delivery-builder.d.ts.map +1 -1
- package/dist/delivery/delivery-builder.js +89 -0
- package/dist/delivery/delivery-builder.js.map +1 -1
- package/dist/delivery/delivery-monitor.d.ts +1 -1
- package/dist/delivery/delivery-monitor.js +1 -1
- package/dist/delivery/delivery-run-control.d.ts +6 -0
- package/dist/delivery/delivery-run-control.d.ts.map +1 -1
- package/dist/delivery/delivery-run-control.js +10 -0
- package/dist/delivery/delivery-run-control.js.map +1 -1
- package/dist/delivery/delivery-supervisor.d.ts.map +1 -1
- package/dist/delivery/delivery-supervisor.js +28 -4
- package/dist/delivery/delivery-supervisor.js.map +1 -1
- package/dist/delivery/delivery-tenant.d.ts +17 -0
- package/dist/delivery/delivery-tenant.d.ts.map +1 -0
- package/dist/delivery/delivery-tenant.js +50 -0
- package/dist/delivery/delivery-tenant.js.map +1 -0
- package/dist/delivery/delivery.d.ts +96 -2
- package/dist/delivery/delivery.d.ts.map +1 -1
- package/dist/delivery/delivery.js +693 -53
- package/dist/delivery/delivery.js.map +1 -1
- package/dist/delivery/inbox-records.d.ts.map +1 -1
- package/dist/delivery/inbox-records.js +9 -16
- package/dist/delivery/inbox-records.js.map +1 -1
- package/dist/delivery/inbox-storage.d.ts +6 -0
- package/dist/delivery/inbox-storage.d.ts.map +1 -1
- package/dist/delivery/inbox-storage.js +43 -6
- package/dist/delivery/inbox-storage.js.map +1 -1
- package/dist/delivery/inbox.d.ts.map +1 -1
- package/dist/delivery/inbox.js +3 -2
- package/dist/delivery/inbox.js.map +1 -1
- package/dist/delivery/reception-observation.d.ts +12 -4
- package/dist/delivery/reception-observation.d.ts.map +1 -1
- package/dist/delivery/reception-observation.js +14 -3
- package/dist/delivery/reception-observation.js.map +1 -1
- package/dist/delivery/sharded-work-registry.d.ts +6 -0
- package/dist/delivery/sharded-work-registry.d.ts.map +1 -1
- package/dist/delivery/sharded-work-registry.js +41 -4
- package/dist/delivery/sharded-work-registry.js.map +1 -1
- package/dist/entity/entity-storage-descriptor.d.ts.map +1 -1
- package/dist/entity/entity-storage-descriptor.js +7 -4
- package/dist/entity/entity-storage-descriptor.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/{bus/saved-dispatcher-binding.js → handler/app-query-handler.js} +8 -12
- 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 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/integration/external-messages.d.ts.map +1 -1
- package/dist/integration/external-messages.js +9 -15
- package/dist/integration/external-messages.js.map +1 -1
- package/dist/integration/integration-broker.d.ts.map +1 -1
- package/dist/integration/integration-broker.js +112 -6
- package/dist/integration/integration-broker.js.map +1 -1
- package/dist/integration/third-party-context.d.ts.map +1 -1
- package/dist/integration/third-party-context.js +7 -10
- package/dist/integration/third-party-context.js.map +1 -1
- package/dist/repository/primitive-id.js +26 -11
- package/dist/repository/primitive-id.js.map +1 -1
- package/dist/repository/repository.d.ts +75 -60
- package/dist/repository/repository.d.ts.map +1 -1
- package/dist/repository/repository.js +1757 -1818
- package/dist/repository/repository.js.map +1 -1
- package/dist/runtime/runtime.d.ts +13 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +18 -0
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/signal-metadata.d.ts.map +1 -1
- package/dist/runtime/signal-metadata.js +6 -6
- package/dist/runtime/signal-metadata.js.map +1 -1
- package/dist/runtime/signal-publisher.d.ts +0 -6
- package/dist/runtime/signal-publisher.d.ts.map +1 -1
- package/dist/runtime/signal-publisher.js +1 -9
- package/dist/runtime/signal-publisher.js.map +1 -1
- package/dist/runtime/standalone-handler-runtime.d.ts +0 -7
- package/dist/runtime/standalone-handler-runtime.d.ts.map +1 -1
- package/dist/runtime/standalone-handler-runtime.js +13 -99
- package/dist/runtime/standalone-handler-runtime.js.map +1 -1
- package/dist/server/managed-delivery-gate.d.ts +34 -0
- package/dist/server/managed-delivery-gate.d.ts.map +1 -0
- package/dist/server/managed-delivery-gate.js +70 -0
- package/dist/server/managed-delivery-gate.js.map +1 -0
- 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 +48 -29
- 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 +220 -25
- package/dist/server/server-environment.d.ts.map +1 -1
- package/dist/server/server-environment.js +734 -145
- package/dist/server/server-environment.js.map +1 -1
- package/dist/server/server-log.d.ts +7 -0
- package/dist/server/server-log.d.ts.map +1 -1
- package/dist/server/server-log.js +14 -1
- package/dist/server/server-log.js.map +1 -1
- package/dist/server/server.d.ts +60 -29
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +155 -201
- 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 +37 -8
- package/dist/services/spine-services.d.ts.map +1 -1
- package/dist/services/spine-services.js +1103 -470
- 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/event-origin-filter.js +3 -3
- package/dist/stand/event-origin-filter.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 +94 -42
- package/dist/stand/stand.d.ts.map +1 -1
- package/dist/stand/stand.js +460 -60
- package/dist/stand/stand.js.map +1 -1
- package/dist/stand/subscription-observer.d.ts.map +1 -1
- package/dist/stand/subscription-observer.js +157 -40
- 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 +54 -15
- package/dist/stand/subscription-registry.d.ts.map +1 -1
- package/dist/stand/subscription-registry.js +164 -41
- package/dist/stand/subscription-registry.js.map +1 -1
- package/dist/stand/subscription-runtime.d.ts +12 -5
- package/dist/stand/subscription-runtime.d.ts.map +1 -1
- package/dist/stand/subscription-runtime.js +183 -34
- package/dist/stand/subscription-runtime.js.map +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +4 -3
- package/dist/testing/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +14 -14
- package/dist/agent/agent-admission.d.ts +0 -80
- package/dist/agent/agent-admission.d.ts.map +0 -1
- package/dist/agent/agent-admission.js +0 -127
- package/dist/agent/agent-admission.js.map +0 -1
- package/dist/agent/agent-ai-runtime.d.ts +0 -76
- package/dist/agent/agent-ai-runtime.d.ts.map +0 -1
- package/dist/agent/agent-ai-runtime.js +0 -1269
- package/dist/agent/agent-ai-runtime.js.map +0 -1
- package/dist/agent/agent-execution-fault.d.ts +0 -18
- package/dist/agent/agent-execution-fault.d.ts.map +0 -1
- package/dist/agent/agent-execution-fault.js.map +0 -1
- package/dist/agent/agent-execution-session.d.ts +0 -103
- package/dist/agent/agent-execution-session.d.ts.map +0 -1
- package/dist/agent/agent-execution-session.js +0 -238
- package/dist/agent/agent-execution-session.js.map +0 -1
- package/dist/agent/agent-interaction-audit.d.ts +0 -58
- package/dist/agent/agent-interaction-audit.d.ts.map +0 -1
- package/dist/agent/agent-interaction-audit.js +0 -114
- package/dist/agent/agent-interaction-audit.js.map +0 -1
- package/dist/agent/agent-interaction-events.d.ts +0 -40
- package/dist/agent/agent-interaction-events.d.ts.map +0 -1
- package/dist/agent/agent-interaction-events.js +0 -129
- package/dist/agent/agent-interaction-events.js.map +0 -1
- package/dist/agent/agent-mcp-budget.d.ts +0 -125
- package/dist/agent/agent-mcp-budget.d.ts.map +0 -1
- package/dist/agent/agent-mcp-budget.js +0 -566
- package/dist/agent/agent-mcp-budget.js.map +0 -1
- package/dist/agent/agent-mcp-host.d.ts +0 -125
- package/dist/agent/agent-mcp-host.d.ts.map +0 -1
- package/dist/agent/agent-mcp-host.js.map +0 -1
- package/dist/agent/agent-mcp-runtime.d.ts.map +0 -1
- package/dist/agent/agent-mcp-runtime.js.map +0 -1
- package/dist/agent/agent-read-runtime.d.ts +0 -70
- package/dist/agent/agent-read-runtime.d.ts.map +0 -1
- package/dist/agent/agent-read-runtime.js +0 -320
- package/dist/agent/agent-read-runtime.js.map +0 -1
- package/dist/agent/agent-revisions.d.ts +0 -35
- package/dist/agent/agent-revisions.d.ts.map +0 -1
- package/dist/agent/agent-revisions.js +0 -133
- package/dist/agent/agent-revisions.js.map +0 -1
- package/dist/agent/agent-scheduler.d.ts +0 -99
- package/dist/agent/agent-scheduler.d.ts.map +0 -1
- package/dist/agent/agent-scheduler.js +0 -398
- package/dist/agent/agent-scheduler.js.map +0 -1
- package/dist/bus/saved-dispatcher-binding.d.ts +0 -53
- package/dist/bus/saved-dispatcher-binding.d.ts.map +0 -1
- package/dist/bus/saved-dispatcher-binding.js.map +0 -1
- package/dist/delivery/delivery-run-coordinator.d.ts +0 -218
- package/dist/delivery/delivery-run-coordinator.d.ts.map +0 -1
- package/dist/delivery/delivery-run-coordinator.js +0 -609
- package/dist/delivery/delivery-run-coordinator.js.map +0 -1
- package/dist/delivery/parked-delivery-obligations.d.ts +0 -182
- package/dist/delivery/parked-delivery-obligations.d.ts.map +0 -1
- package/dist/delivery/parked-delivery-obligations.js +0 -390
- package/dist/delivery/parked-delivery-obligations.js.map +0 -1
- package/dist/server/environment-attachment.d.ts +0 -356
- package/dist/server/environment-attachment.d.ts.map +0 -1
- package/dist/server/environment-attachment.js +0 -2062
- package/dist/server/environment-attachment.js.map +0 -1
- package/dist/server/environment-delivery-records.d.ts +0 -77
- package/dist/server/environment-delivery-records.d.ts.map +0 -1
- package/dist/server/environment-delivery-records.js +0 -282
- package/dist/server/environment-delivery-records.js.map +0 -1
- package/dist/server/environment-delivery-worker.d.ts +0 -161
- package/dist/server/environment-delivery-worker.d.ts.map +0 -1
- package/dist/server/environment-delivery-worker.js +0 -973
- package/dist/server/environment-delivery-worker.js.map +0 -1
|
@@ -11,36 +11,47 @@
|
|
|
11
11
|
* or implied. See the License for the specific language governing permissions and limitations under
|
|
12
12
|
* the License.
|
|
13
13
|
*/
|
|
14
|
+
var _a;
|
|
14
15
|
import { randomUUID } from "node:crypto";
|
|
15
16
|
import { Code, ConnectError } from "@connectrpc/connect";
|
|
16
|
-
import { clone, create,
|
|
17
|
+
import { clone, create, equals, toBinary, ScalarType, } from "@bufbuild/protobuf";
|
|
18
|
+
import { ReaderIdentities, snapshotTrustedReader, } from "@spine-event-engine/core/spi/trusted-reader";
|
|
17
19
|
import { AnySchema, BoolValueSchema, BytesValueSchema, DoubleValueSchema, EmptySchema, FloatValueSchema, Int32ValueSchema, Int64ValueSchema, StringValueSchema, UInt32ValueSchema, UInt64ValueSchema, } from "@bufbuild/protobuf/wkt";
|
|
18
|
-
import { TypeUrls,
|
|
19
|
-
import { CommandIdSchema,
|
|
20
|
+
import { TypeUrls, AnyPacker, Validate } from "@spine-event-engine/core";
|
|
21
|
+
import { CommandIdSchema, CommandValidationError as CommandValidationErrorCode, TenantIdSchema, ValidationErrorSchema, VersionSchema, } from "@spine-event-engine/proto";
|
|
20
22
|
import { ErrorSchema } from "@spine-event-engine/proto";
|
|
21
23
|
import { CommandService } from "@spine-event-engine/proto/client";
|
|
22
24
|
import { CompositeFilter_CompositeOperator, Filter_Operator, } from "@spine-event-engine/proto/client";
|
|
23
|
-
import { EntityStateWithVersionSchema as EntityStateVersionSchema, OrderBy_Direction, QueryResponseSchema, } from "@spine-event-engine/proto/client";
|
|
25
|
+
import { EntityStateWithVersionSchema as EntityStateVersionSchema, OrderBy_Direction, QueryResponseSchema, QuerySchema, } from "@spine-event-engine/proto/client";
|
|
24
26
|
import { QueryService } from "@spine-event-engine/proto/client";
|
|
25
|
-
import {
|
|
27
|
+
import { SubscriptionIdSchema, SubscriptionSchema, SubscriptionUpdateSchema, TopicSchema, } from "@spine-event-engine/proto/client";
|
|
26
28
|
import { SubscriptionService } from "@spine-event-engine/proto/client";
|
|
27
29
|
import { EventOriginFilters } from "../stand/event-origin-filter.js";
|
|
30
|
+
import { TenantAdmissions, TenantUnknownError } from "../context/tenant-unknown-error.js";
|
|
28
31
|
import { AckSchema } from "@spine-event-engine/proto";
|
|
29
32
|
import { ResponseSchema, StatusSchema, } from "@spine-event-engine/proto";
|
|
30
33
|
import {} from "@spine-event-engine/storage";
|
|
31
34
|
import { TenantBoundary } from "@spine-event-engine/storage/provider";
|
|
32
|
-
import { boundedContextAccess } from "../context/bounded-context.js";
|
|
35
|
+
import { BoundedContext, boundedContextAccess } from "../context/bounded-context.js";
|
|
33
36
|
import { EffectiveTenants } from "../context/effective-tenant.js";
|
|
34
37
|
import { CommandValidationError } from "../bus/command-errors.js";
|
|
35
38
|
import { TransitionValidationError } from "../repository/command-errors.js";
|
|
36
39
|
import {} from "../stand/stand.js";
|
|
37
40
|
import { QueryReader } from "./query-reader.js";
|
|
41
|
+
import { AppQueryExecution } from "./app-query-execution.js";
|
|
42
|
+
import { ReadRequestStatus } from "./read-request-status.js";
|
|
43
|
+
import { TrustedReaderIngress } from "./trusted-reader-ingress.js";
|
|
38
44
|
import { RegisteredTargets } from "./registered-targets.js";
|
|
39
45
|
import { emitServerError, emitServerWarning } from "../server/server-log.js";
|
|
46
|
+
import { ServerEnvironment } from "../server/server-environment.js";
|
|
40
47
|
import { managedChildSubscriptionAccess } from "../server/managed-child-subscription.js";
|
|
41
|
-
import {
|
|
48
|
+
import {} from "../stand/subscription-registry.js";
|
|
49
|
+
import { ReadSideFilterEngine } from "../stand/read-side-filter-engine.js";
|
|
50
|
+
import { SubscriptionReadFilters, } from "../stand/subscription-read-filters.js";
|
|
42
51
|
const serviceLoggers = new WeakMap();
|
|
43
52
|
const serviceInstances = new WeakSet();
|
|
53
|
+
const serviceAppQueries = new WeakMap();
|
|
54
|
+
const serviceReadFilters = new WeakMap();
|
|
44
55
|
/**
|
|
45
56
|
* Small route registrar for the first public Spine gRPC service slice.
|
|
46
57
|
*
|
|
@@ -57,7 +68,8 @@ const serviceInstances = new WeakSet();
|
|
|
57
68
|
* Queries allow at most 100 IDs, 16 simple filters, 8 composites, and 8 orderings.
|
|
58
69
|
* Use proto column names such as `open_task_count`, not generated TS local names
|
|
59
70
|
* such as `openTaskCount`. Invalid columns, values, operators, and query shapes
|
|
60
|
-
*
|
|
71
|
+
* reject with structured `INVALID_ARGUMENT` and `INVALID_QUERY` details before
|
|
72
|
+
* Stand storage is read.
|
|
61
73
|
*
|
|
62
74
|
* `SubscriptionService.Subscribe` accepts known state targets with
|
|
63
75
|
* `include_all` or validated ID/field filters and known event targets exposed
|
|
@@ -73,7 +85,9 @@ const serviceInstances = new WeakSet();
|
|
|
73
85
|
* rejected-command payload forms and stack trace from their client-facing
|
|
74
86
|
* context.
|
|
75
87
|
* Application code remains on generated domain event messages through handler
|
|
76
|
-
* dispatch. Unknown or
|
|
88
|
+
* dispatch. Unknown or expired activation IDs fail with `NOT_FOUND` and
|
|
89
|
+
* `UNKNOWN_SUBSCRIPTION` details. A duplicate active activation completes
|
|
90
|
+
* without another update stream.
|
|
77
91
|
* Cancellation of unknown or already-cleaned IDs returns OK after admission to
|
|
78
92
|
* the bounded unknown-removal pool.
|
|
79
93
|
*/
|
|
@@ -86,9 +100,10 @@ export class SpineServices {
|
|
|
86
100
|
#activationTails = new Map();
|
|
87
101
|
#removals = new Map();
|
|
88
102
|
#unknownRemovals = new Set();
|
|
89
|
-
#
|
|
90
|
-
#queueLimit;
|
|
103
|
+
#queueCapacity;
|
|
91
104
|
#subscriptionLimit;
|
|
105
|
+
#readerAdmission;
|
|
106
|
+
#readMaxBytes;
|
|
92
107
|
/**
|
|
93
108
|
* Creates service adapters over built bounded contexts.
|
|
94
109
|
*
|
|
@@ -97,8 +112,10 @@ export class SpineServices {
|
|
|
97
112
|
constructor(options) {
|
|
98
113
|
serviceInstances.add(this);
|
|
99
114
|
this.#contexts = Object.freeze([...options.contexts]);
|
|
100
|
-
this.#
|
|
115
|
+
this.#queueCapacity = ServerEnvironment.instance().subscriptionQueueCapacity;
|
|
101
116
|
this.#subscriptionLimit = ServiceValues.subscriptionLimit(options.subscriptionLimit ?? ServiceValues.defaultSubscriptionLimit);
|
|
117
|
+
this.#readerAdmission = options.readerAdmission;
|
|
118
|
+
this.#readMaxBytes = options.readMaxBytes ?? 4_194_304;
|
|
102
119
|
this.#registerSignalRoutes();
|
|
103
120
|
this.#registerStateRoutes(new RegisteredTargets(this.#contexts));
|
|
104
121
|
}
|
|
@@ -151,18 +168,119 @@ export class SpineServices {
|
|
|
151
168
|
* @returns Returns the registered router.
|
|
152
169
|
*/
|
|
153
170
|
register(router) {
|
|
171
|
+
this.#registerUnary(router);
|
|
172
|
+
this.#registerSubscriptions(router);
|
|
173
|
+
return router;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Registers admitted Command and Query routes.
|
|
177
|
+
*
|
|
178
|
+
* @param router Connect service router.
|
|
179
|
+
*/
|
|
180
|
+
#registerUnary(router) {
|
|
154
181
|
router.service(CommandService, {
|
|
155
|
-
post: (command) =>
|
|
182
|
+
post: async (command, context) => {
|
|
183
|
+
await this.#admit("spine.client.CommandService", "Post", toBinary(CommandService.method.post.input, command), context);
|
|
184
|
+
return this.#post(command);
|
|
185
|
+
},
|
|
156
186
|
});
|
|
157
187
|
router.service(QueryService, {
|
|
158
|
-
read: (query) =>
|
|
188
|
+
read: async (query, context) => {
|
|
189
|
+
const detached = clone(QuerySchema, query);
|
|
190
|
+
const reader = await this.#admit("spine.client.QueryService", "Read", toBinary(QuerySchema, detached), context);
|
|
191
|
+
return this.#read(detached, reader, context?.signal);
|
|
192
|
+
},
|
|
193
|
+
execute: async (query, context) => {
|
|
194
|
+
const detached = clone(QueryService.method.execute.input, query);
|
|
195
|
+
const reader = await this.#admit("spine.client.QueryService", "Execute", toBinary(QueryService.method.execute.input, detached), context);
|
|
196
|
+
return this.#executeAppQuery(detached, reader, context.signal);
|
|
197
|
+
},
|
|
159
198
|
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Executes a detached App Query with facts established by a trusted admission port.
|
|
202
|
+
*
|
|
203
|
+
* @param query Captured application query envelope.
|
|
204
|
+
* @param reader Verified current invocation facts.
|
|
205
|
+
* @param signal Current invocation cancellation, when available.
|
|
206
|
+
* @returns One paired response from the Server-selected handler.
|
|
207
|
+
*/
|
|
208
|
+
#executeAppQuery(query, reader, signal) {
|
|
209
|
+
const queries = serviceAppQueries.get(this);
|
|
210
|
+
if (queries === undefined)
|
|
211
|
+
return Promise.reject(new ConnectError("App Query execution is unavailable.", Code.Unimplemented));
|
|
212
|
+
return queries.execute(query, reader, signal);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Registers reader-bound subscription routes.
|
|
216
|
+
*
|
|
217
|
+
* @param router Connect service router.
|
|
218
|
+
*/
|
|
219
|
+
#registerSubscriptions(router) {
|
|
160
220
|
router.service(SubscriptionService, {
|
|
161
|
-
subscribe: (topic) => this.#
|
|
162
|
-
activate: (subscription) => this.#
|
|
163
|
-
cancel: (subscription) => this.#
|
|
221
|
+
subscribe: (topic, context) => this.#subscribeAdmitted(topic, context),
|
|
222
|
+
activate: (subscription, context) => this.#activateAdmitted(subscription, context),
|
|
223
|
+
cancel: (subscription, context) => this.#cancelAdmitted(subscription, context),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Keeps direct in-process Subscribe validation synchronous while admitting native peers.
|
|
228
|
+
*
|
|
229
|
+
* @param topic Requested subscription topic.
|
|
230
|
+
* @param context Native transport context, absent for direct in-process calls.
|
|
231
|
+
* @returns Subscription or admitted native operation promise.
|
|
232
|
+
*/
|
|
233
|
+
#subscribeAdmitted(topic, context) {
|
|
234
|
+
if (context === undefined)
|
|
235
|
+
return this.#subscribe(topic, { kind: "anonymous" });
|
|
236
|
+
return this.#admit("spine.client.SubscriptionService", "Subscribe", toBinary(TopicSchema, topic), context).then((reader) => this.#subscribe(topic, ReaderIdentities.from(reader)));
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Keeps direct in-process Cancel synchronous while admitting native peers.
|
|
240
|
+
*
|
|
241
|
+
* @param subscription Requested subscription definition.
|
|
242
|
+
* @param context Native transport context, absent for direct in-process calls.
|
|
243
|
+
* @returns Cancel acknowledgment or admitted native operation promise.
|
|
244
|
+
*/
|
|
245
|
+
#cancelAdmitted(subscription, context) {
|
|
246
|
+
if (context === undefined)
|
|
247
|
+
return this.#cancel(subscription, { kind: "anonymous" });
|
|
248
|
+
return this.#admit("spine.client.SubscriptionService", "Cancel", toBinary(SubscriptionSchema, subscription), context).then((reader) => this.#cancel(subscription, ReaderIdentities.from(reader)));
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Verifies one native reader assertion before service side effects.
|
|
252
|
+
*
|
|
253
|
+
* @param service Canonical RPC service.
|
|
254
|
+
* @param method Canonical RPC method.
|
|
255
|
+
* @param bytes Canonical request bytes.
|
|
256
|
+
* @param context Native transport context when called through Connect.
|
|
257
|
+
* @returns Detached trusted reader facts.
|
|
258
|
+
*/
|
|
259
|
+
async #admit(service, method, bytes, context) {
|
|
260
|
+
if (context === undefined)
|
|
261
|
+
return Object.freeze({});
|
|
262
|
+
return TrustedReaderIngress.admit({
|
|
263
|
+
service,
|
|
264
|
+
method,
|
|
265
|
+
request: bytes,
|
|
266
|
+
headers: context.requestHeader,
|
|
267
|
+
peer: context,
|
|
268
|
+
maxBytes: this.#readMaxBytes,
|
|
269
|
+
...this.#readerAdmission,
|
|
164
270
|
});
|
|
165
|
-
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Verifies activation before entering the subscription lifecycle generator.
|
|
274
|
+
*
|
|
275
|
+
* @param subscription Native activation request.
|
|
276
|
+
* @param context Native transport context.
|
|
277
|
+
* @returns Admitted subscription update stream.
|
|
278
|
+
*/
|
|
279
|
+
async *#activateAdmitted(subscription, context) {
|
|
280
|
+
const captured = clone(SubscriptionSchema, subscription);
|
|
281
|
+
const signal = context?.signal;
|
|
282
|
+
const reader = await this.#admit("spine.client.SubscriptionService", "Activate", toBinary(SubscriptionSchema, captured), context);
|
|
283
|
+
yield* this.#activate(captured, snapshotTrustedReader(reader), signal);
|
|
166
284
|
}
|
|
167
285
|
/**
|
|
168
286
|
* Resolves a public Command route before attempting dispatch.
|
|
@@ -171,14 +289,29 @@ export class SpineServices {
|
|
|
171
289
|
* @returns Acknowledgement for route validation or dispatch.
|
|
172
290
|
*/
|
|
173
291
|
async #post(command) {
|
|
174
|
-
const messageId = command.id &&
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
292
|
+
const messageId = command.id && AnyPacker.pack(CommandIdSchema, command.id);
|
|
293
|
+
try {
|
|
294
|
+
CommandValidationError.checkEnvelope(command);
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
const failure = ServiceValues.commandPostError(error);
|
|
298
|
+
return ServiceValues.commandErrorAck(messageId, failure.type, failure.message, failure.code, failure.details);
|
|
299
|
+
}
|
|
300
|
+
const typeUrl = command.message.typeUrl;
|
|
178
301
|
const admission = this.#commandAdmission(command, messageId, typeUrl);
|
|
179
302
|
if ("ack" in admission)
|
|
180
303
|
return admission.ack;
|
|
181
|
-
|
|
304
|
+
return this.#dispatchCommand(admission.route, command, messageId);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Dispatches an admitted Command and maps its completed or failed outcome.
|
|
308
|
+
*
|
|
309
|
+
* @param route Selected Command route.
|
|
310
|
+
* @param command Validated Command envelope.
|
|
311
|
+
* @param messageId Packed ID for the acknowledgement.
|
|
312
|
+
* @returns The completed Command acknowledgement.
|
|
313
|
+
*/
|
|
314
|
+
async #dispatchCommand(route, command, messageId) {
|
|
182
315
|
try {
|
|
183
316
|
await route.context.commandBus().post(command);
|
|
184
317
|
return create(AckSchema, {
|
|
@@ -187,11 +320,13 @@ export class SpineServices {
|
|
|
187
320
|
});
|
|
188
321
|
}
|
|
189
322
|
catch (error) {
|
|
323
|
+
if (error instanceof TenantUnknownError)
|
|
324
|
+
throw TenantStatuses.unknown(error);
|
|
190
325
|
const postError = ServiceValues.commandPostError(error);
|
|
191
326
|
this.#logCommandPostFailure(route, command, error);
|
|
192
327
|
return create(AckSchema, {
|
|
193
328
|
messageId,
|
|
194
|
-
status: ServiceValues.errorStatus(postError.type, postError.message, postError.details),
|
|
329
|
+
status: ServiceValues.errorStatus(postError.type, postError.message, postError.details, postError.code),
|
|
195
330
|
});
|
|
196
331
|
}
|
|
197
332
|
}
|
|
@@ -207,7 +342,7 @@ export class SpineServices {
|
|
|
207
342
|
const route = this.#commandRoutes.get(typeUrl);
|
|
208
343
|
if (route === undefined) {
|
|
209
344
|
return {
|
|
210
|
-
ack: ServiceValues.commandErrorAck(messageId, "
|
|
345
|
+
ack: ServiceValues.commandErrorAck(messageId, "spine.core.CommandValidationError", "No bounded context accepted the command.", CommandValidationErrorCode.UNSUPPORTED_COMMAND),
|
|
211
346
|
};
|
|
212
347
|
}
|
|
213
348
|
const tenantError = ServiceValues.tenantMismatch(route.context.isMultitenant, ServiceValues.commandTenant(command), "command");
|
|
@@ -244,19 +379,23 @@ export class SpineServices {
|
|
|
244
379
|
* Validates a public state query before reading its registered Stand.
|
|
245
380
|
*
|
|
246
381
|
* @param query Public query request.
|
|
247
|
-
* @
|
|
382
|
+
* @param reader Verified facts from current admission.
|
|
383
|
+
* @param signal Current request cancellation.
|
|
384
|
+
* @returns States for an admitted Query.
|
|
385
|
+
* @throws ConnectError for invalid input or a sanitized read failure.
|
|
248
386
|
*/
|
|
249
|
-
async #read(query) {
|
|
387
|
+
async #read(query, reader, signal) {
|
|
388
|
+
ReadRequestStatus.check("query", () => Validate.check(QuerySchema, query));
|
|
250
389
|
const target = query.target;
|
|
251
390
|
const route = this.#readRoute(target);
|
|
252
391
|
if (target === undefined || route === undefined) {
|
|
253
|
-
|
|
392
|
+
throw ReadRequestStatus.invalid("query", true, "No bounded context owns query target.");
|
|
254
393
|
}
|
|
255
394
|
const queryError = ServiceValues.validateReadQuery(query, target, route);
|
|
256
395
|
if (queryError !== undefined) {
|
|
257
|
-
|
|
396
|
+
throw ReadRequestStatus.invalid("query", false, queryError.message);
|
|
258
397
|
}
|
|
259
|
-
return this.#readValidated(query, target, route);
|
|
398
|
+
return this.#readValidated(query, target, route, reader, signal);
|
|
260
399
|
}
|
|
261
400
|
/**
|
|
262
401
|
* Resolves the effective tenant and executes an already validated read.
|
|
@@ -264,41 +403,96 @@ export class SpineServices {
|
|
|
264
403
|
* @param query Validated public query.
|
|
265
404
|
* @param target Registered query target.
|
|
266
405
|
* @param route Route containing the target Stand and schema.
|
|
267
|
-
* @
|
|
406
|
+
* @param reader Verified current invocation facts.
|
|
407
|
+
* @param signal Current request cancellation.
|
|
408
|
+
* @returns States when tenant admission and storage read succeed.
|
|
268
409
|
*/
|
|
269
|
-
async #readValidated(query, target, route) {
|
|
410
|
+
async #readValidated(query, target, route, reader, signal) {
|
|
270
411
|
const tenantId = ServiceValues.tenantValue(query.context?.tenantId);
|
|
271
412
|
const tenantError = ServiceValues.tenantMismatch(route.context.isMultitenant, tenantId, "query");
|
|
272
413
|
if (tenantError !== undefined) {
|
|
273
|
-
|
|
414
|
+
throw ReadRequestStatus.tenant(tenantError.type, tenantError.message);
|
|
274
415
|
}
|
|
275
416
|
try {
|
|
276
417
|
const effective = EffectiveTenants.current(route.context.isMultitenant, tenantId);
|
|
277
418
|
const destination = EffectiveTenants.destination(effective, route.context.isMultitenant);
|
|
278
|
-
return await this.#query(route, ServiceValues.createReadPlan(target, query, route), destination);
|
|
419
|
+
return await this.#query(route, ServiceValues.createReadPlan(target, query, route), destination, query, reader, signal);
|
|
279
420
|
}
|
|
280
|
-
catch {
|
|
281
|
-
|
|
421
|
+
catch (error) {
|
|
422
|
+
if (error instanceof TenantUnknownError)
|
|
423
|
+
throw TenantStatuses.unknown(error);
|
|
424
|
+
throw ReadRequestStatus.technical(error, "Query read failed.");
|
|
282
425
|
}
|
|
283
426
|
}
|
|
284
|
-
|
|
427
|
+
/**
|
|
428
|
+
* Finds the registered Entity state route for a query target.
|
|
429
|
+
*
|
|
430
|
+
* @param target Declared query or Topic target.
|
|
431
|
+
* @returns The matching read route, if present.
|
|
432
|
+
*/ #readRoute(target) {
|
|
285
433
|
return target === undefined ? undefined : this.#stateRoutes.get(target.type);
|
|
286
434
|
}
|
|
287
|
-
|
|
288
|
-
|
|
435
|
+
/**
|
|
436
|
+
* Executes a validated Entity state read through its registered route.
|
|
437
|
+
*
|
|
438
|
+
* @param route Registered target route with schema and tenancy.
|
|
439
|
+
* @param plan Validated Entity read plan.
|
|
440
|
+
* @param tenantId Declared Proto tenant identifier.
|
|
441
|
+
* @param query Validated public query with effective actor data.
|
|
442
|
+
* @param reader Verified current invocation facts.
|
|
443
|
+
* @param signal Current request cancellation.
|
|
444
|
+
* @returns The Entity query response.
|
|
445
|
+
*/ async #query(route, plan, tenantId, query, reader, signal) {
|
|
446
|
+
const facts = {
|
|
447
|
+
operation: { kind: "query", query },
|
|
448
|
+
...(query.context === undefined ? {} : { actorContext: query.context }),
|
|
449
|
+
...(tenantId === undefined ? {} : { effectiveTenant: tenantId }),
|
|
450
|
+
producingContext: route.context,
|
|
451
|
+
...reader,
|
|
452
|
+
...(signal === undefined ? {} : { signal }),
|
|
453
|
+
};
|
|
454
|
+
const results = await QueryReader.read(route.context.stand(), route.schema, plan, tenantId, ServiceValues.queryResultLimit, query, facts);
|
|
455
|
+
const visible = await this.#visibleStates(route, results, facts);
|
|
289
456
|
return create(QueryResponseSchema, {
|
|
290
457
|
response: ServiceValues.okResponse(),
|
|
291
|
-
message:
|
|
458
|
+
message: visible.map((result) => ServiceValues.packVersionedState(route.schema, result)),
|
|
292
459
|
});
|
|
293
460
|
}
|
|
294
|
-
|
|
461
|
+
/**
|
|
462
|
+
* Applies this Server's outward policy after BC source policy and storage work.
|
|
463
|
+
*
|
|
464
|
+
* @param route Registered Entity result schema.
|
|
465
|
+
* @param results States returned by the producing context.
|
|
466
|
+
* @param facts Verified current Query facts.
|
|
467
|
+
* @returns Surviving states without replenishing removed records.
|
|
468
|
+
*/
|
|
469
|
+
async #visibleStates(route, results, facts) {
|
|
470
|
+
const filters = serviceReadFilters.get(this);
|
|
471
|
+
if (filters === undefined)
|
|
472
|
+
return results;
|
|
473
|
+
const visible = [];
|
|
474
|
+
for (const result of results) {
|
|
475
|
+
const state = await filters.apply(route.schema, result.state, facts);
|
|
476
|
+
if (state !== null)
|
|
477
|
+
visible.push({ ...result, state });
|
|
478
|
+
}
|
|
479
|
+
return visible;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Validates a topic synchronously and checks live membership before registry creation.
|
|
483
|
+
*
|
|
484
|
+
* @param topic Requested subscription topic.
|
|
485
|
+
* @returns Created subscription after admission.
|
|
486
|
+
* @param reader The verified reader facts for this operation.
|
|
487
|
+
*/
|
|
488
|
+
#subscribe(topic, reader) {
|
|
489
|
+
ReadRequestStatus.check("topic", () => Validate.check(TopicSchema, topic));
|
|
295
490
|
const route = this.#subscriptionRoute(topic);
|
|
296
|
-
ServiceValues.
|
|
297
|
-
const shape = ServiceValues.createSubscriptionShape(topic, route);
|
|
491
|
+
ServiceValues.checkTopicSemantics(topic, route);
|
|
298
492
|
const tenantId = ServiceValues.topicTenant(topic);
|
|
299
493
|
const tenantError = ServiceValues.tenantMismatch(route.context.isMultitenant, tenantId, "subscription");
|
|
300
494
|
if (tenantError !== undefined) {
|
|
301
|
-
throw
|
|
495
|
+
throw ReadRequestStatus.tenant(tenantError.type, tenantError.message);
|
|
302
496
|
}
|
|
303
497
|
const subscription = create(SubscriptionSchema, {
|
|
304
498
|
id: create(SubscriptionIdSchema, { value: `s-${randomUUID()}` }),
|
|
@@ -308,50 +502,117 @@ export class SpineServices {
|
|
|
308
502
|
if (id === undefined) {
|
|
309
503
|
throw new ConnectError("Subscription ID is required.", Code.InvalidArgument);
|
|
310
504
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
return registry.create(subscription).then(() => clone(SubscriptionSchema, subscription));
|
|
505
|
+
const registry = boundedContextAccess.subscriptionRegistry(route.context);
|
|
506
|
+
return TenantStatuses.require(route.context, tenantId, async () => {
|
|
507
|
+
await registry.create(subscription, reader);
|
|
508
|
+
}).then(() => clone(SubscriptionSchema, subscription));
|
|
317
509
|
}
|
|
318
|
-
|
|
510
|
+
/**
|
|
511
|
+
* Verifies membership and activates a retained subscription.
|
|
512
|
+
*
|
|
513
|
+
* @param subscription The subscription definition being processed.
|
|
514
|
+
* @param reader The verified reader facts for this operation.
|
|
515
|
+
* @returns The update stream after admission.
|
|
516
|
+
*/ async *#activate(subscription, reader, signal) {
|
|
517
|
+
ReadRequestStatus.check("subscription", () => Validate.check(SubscriptionSchema, subscription));
|
|
518
|
+
ReadRequestStatus.checkLifecycleCriterion(subscription);
|
|
319
519
|
const id = subscription.id?.value;
|
|
320
520
|
if (id === undefined) {
|
|
321
521
|
return;
|
|
322
522
|
}
|
|
323
|
-
const record = await this.#activationRecord(id);
|
|
523
|
+
const record = await this.#activationRecord(id, ReaderIdentities.from(reader), signal);
|
|
324
524
|
if (record === undefined)
|
|
325
525
|
return;
|
|
526
|
+
yield* this.#streamSubscription(record, reader);
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Delivers one activation in source order while honoring its cancellation.
|
|
530
|
+
*
|
|
531
|
+
* @param record Attached subscription and existing admission queue.
|
|
532
|
+
* @param reader Verified facts of this activation.
|
|
533
|
+
* @returns ACK followed by surviving filtered data updates.
|
|
534
|
+
*/
|
|
535
|
+
async *#streamSubscription(record, reader) {
|
|
326
536
|
try {
|
|
537
|
+
const facts = _a.#subscriptionFacts(record, reader);
|
|
538
|
+
if (record.delivery.closed)
|
|
539
|
+
return;
|
|
327
540
|
yield create(SubscriptionUpdateSchema, {
|
|
328
541
|
subscription: clone(SubscriptionSchema, record.subscription),
|
|
329
542
|
response: ServiceValues.okResponse(),
|
|
330
543
|
});
|
|
331
544
|
for (;;) {
|
|
332
|
-
const
|
|
333
|
-
if (
|
|
545
|
+
const work = await record.delivery.next();
|
|
546
|
+
if (work === undefined)
|
|
547
|
+
return;
|
|
548
|
+
const update = await record.delivery.settle(this.#filteredSubscription(work, facts));
|
|
549
|
+
if (update === undefined)
|
|
334
550
|
return;
|
|
551
|
+
if (update !== null) {
|
|
552
|
+
if (record.delivery.signal.aborted)
|
|
553
|
+
return;
|
|
554
|
+
yield update;
|
|
335
555
|
}
|
|
336
|
-
|
|
556
|
+
record.delivery.advance();
|
|
337
557
|
}
|
|
338
558
|
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
record.delivery.close();
|
|
561
|
+
throw ReadRequestStatus.technical(error, "Subscription filtering failed.");
|
|
562
|
+
}
|
|
339
563
|
finally {
|
|
340
|
-
if (
|
|
341
|
-
|
|
564
|
+
if (this.#subscriptions.get(record.id) === record) {
|
|
565
|
+
if (record.delivery.closed)
|
|
566
|
+
this.#removeDetachedSubscription(record.id);
|
|
567
|
+
else
|
|
568
|
+
await this.#removeSubscription(record.id);
|
|
342
569
|
}
|
|
343
570
|
}
|
|
344
571
|
}
|
|
572
|
+
/**
|
|
573
|
+
* Captures the original Topic and authenticated activation facts for policy code.
|
|
574
|
+
*
|
|
575
|
+
* @param record Current source route and canonical subscription.
|
|
576
|
+
* @param reader Verified facts of this activation.
|
|
577
|
+
* @returns Subscription operation context with a live cancellation signal.
|
|
578
|
+
*/
|
|
579
|
+
static #subscriptionFacts(record, reader) {
|
|
580
|
+
const topic = record.subscription.topic;
|
|
581
|
+
if (topic === undefined)
|
|
582
|
+
throw new TypeError("Activated subscription Topic is missing.");
|
|
583
|
+
return {
|
|
584
|
+
operation: { kind: "subscription", subscription: record.subscription, topic },
|
|
585
|
+
...(topic.context === undefined ? {} : { actorContext: topic.context }),
|
|
586
|
+
...(record.tenantId === undefined ? {} : { effectiveTenant: record.tenantId }),
|
|
587
|
+
producingContext: record.route.context,
|
|
588
|
+
...reader,
|
|
589
|
+
signal: record.delivery.signal,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Applies the producing BC policy before this Server's final policy.
|
|
594
|
+
*
|
|
595
|
+
* @param work Queued matched update and lazy BC capability.
|
|
596
|
+
* @param facts Trusted activation and original Topic facts.
|
|
597
|
+
* @returns Surviving update or null for an empty domain-data batch.
|
|
598
|
+
*/
|
|
599
|
+
async #filteredSubscription(work, facts) {
|
|
600
|
+
const source = await work.filter(facts);
|
|
601
|
+
return source === null
|
|
602
|
+
? null
|
|
603
|
+
: SubscriptionReadFilters.apply(source, serviceReadFilters.get(this), facts);
|
|
604
|
+
}
|
|
345
605
|
/**
|
|
346
606
|
* Serializes one native activation and returns its attached record.
|
|
347
607
|
*
|
|
348
608
|
* @param id The logical subscription identifier.
|
|
349
609
|
* @returns The attached record, or undefined when admission fails.
|
|
610
|
+
* @param reader The verified reader facts for this operation.
|
|
350
611
|
*/
|
|
351
|
-
async #activationRecord(id) {
|
|
612
|
+
async #activationRecord(id, reader, signal) {
|
|
352
613
|
let attached;
|
|
353
614
|
await this.#serializeActivation(id, async () => {
|
|
354
|
-
attached = await this.#startActivationRecord(id);
|
|
615
|
+
attached = await this.#startActivationRecord(id, reader, signal);
|
|
355
616
|
});
|
|
356
617
|
return attached;
|
|
357
618
|
}
|
|
@@ -360,23 +621,52 @@ export class SpineServices {
|
|
|
360
621
|
*
|
|
361
622
|
* @param id The logical subscription identifier.
|
|
362
623
|
* @returns The attached record, or undefined when unavailable.
|
|
624
|
+
* @param reader The verified reader facts for this operation.
|
|
363
625
|
*/
|
|
364
|
-
async #startActivationRecord(id) {
|
|
626
|
+
async #startActivationRecord(id, reader, signal) {
|
|
365
627
|
const record = this.#subscriptions.get(id) ?? (await this.#findSubscription(id));
|
|
366
|
-
if (record === undefined
|
|
628
|
+
if (record === undefined)
|
|
629
|
+
throw ReadRequestStatus.unknownSubscription();
|
|
630
|
+
if (!ReaderIdentities.matches(record.reader, reader))
|
|
631
|
+
throw this.#readerDenied();
|
|
632
|
+
let attached;
|
|
633
|
+
await this.#requireSubscriptionTenant(record, async () => {
|
|
634
|
+
attached = await this.#activateKnownRecord(record, id, reader, signal);
|
|
635
|
+
});
|
|
636
|
+
return attached;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Activates and attaches one admitted record before its registry closes.
|
|
640
|
+
*
|
|
641
|
+
* @param record Discovered subscription record.
|
|
642
|
+
* @param id Canonical subscription ID.
|
|
643
|
+
* @returns Attached record, if activation remains eligible.
|
|
644
|
+
* @param reader The verified reader facts for this operation.
|
|
645
|
+
*/
|
|
646
|
+
async #activateKnownRecord(record, id, reader, signal) {
|
|
647
|
+
if (record.delivery.active)
|
|
367
648
|
return undefined;
|
|
368
|
-
const activation = await
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
649
|
+
const activation = await boundedContextAccess
|
|
650
|
+
.subscriptionRegistry(record.route.context)
|
|
651
|
+
.activate(create(SubscriptionIdSchema, { value: id }), reader);
|
|
652
|
+
if (activation.kind === "denied")
|
|
653
|
+
throw this.#readerDenied();
|
|
654
|
+
if (!("entry" in activation))
|
|
655
|
+
throw ReadRequestStatus.unknownSubscription();
|
|
656
|
+
if (!equals(SubscriptionSchema, record.subscription, activation.entry.subscription))
|
|
657
|
+
throw ReadRequestStatus.unknownSubscription();
|
|
658
|
+
if (record.delivery.closed)
|
|
373
659
|
return undefined;
|
|
374
660
|
try {
|
|
375
661
|
this.#subscriptions.set(id, record);
|
|
376
|
-
|
|
662
|
+
record.delivery.track(signal);
|
|
663
|
+
await this.#activateRecord(record, activation.entry);
|
|
664
|
+
await this.#checkAttachedRecord(record, id, reader, signal);
|
|
377
665
|
return record;
|
|
378
666
|
}
|
|
379
667
|
catch (error) {
|
|
668
|
+
if (record.delivery.invalidated)
|
|
669
|
+
throw error;
|
|
380
670
|
try {
|
|
381
671
|
await this.#removeSubscription(id);
|
|
382
672
|
}
|
|
@@ -386,7 +676,30 @@ export class SpineServices {
|
|
|
386
676
|
throw error;
|
|
387
677
|
}
|
|
388
678
|
}
|
|
389
|
-
|
|
679
|
+
/**
|
|
680
|
+
* Rejects an entry removed or replaced before its first activation ACK.
|
|
681
|
+
* @param record Attached delivery that may have been invalidated.
|
|
682
|
+
* @param id Canonical subscription ID.
|
|
683
|
+
* @param reader Reader admitted by the earlier activation.
|
|
684
|
+
* @param signal Transport cancellation, which ends the stream normally.
|
|
685
|
+
*/
|
|
686
|
+
async #checkAttachedRecord(record, id, reader, signal) {
|
|
687
|
+
if (!record.delivery.invalidated || signal?.aborted === true)
|
|
688
|
+
return;
|
|
689
|
+
const current = await boundedContextAccess
|
|
690
|
+
.subscriptionRegistry(record.route.context)
|
|
691
|
+
.get(create(SubscriptionIdSchema, { value: id }));
|
|
692
|
+
if (current !== undefined && !ReaderIdentities.matches(current.reader, reader))
|
|
693
|
+
throw this.#readerDenied();
|
|
694
|
+
throw ReadRequestStatus.unknownSubscription();
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Serializes activation callbacks for the same subscription ID.
|
|
698
|
+
*
|
|
699
|
+
* @param id The stable identifier for this record.
|
|
700
|
+
* @param work Accepted lifecycle operation.
|
|
701
|
+
* @returns Completion after the operation settles.
|
|
702
|
+
*/ async #serializeActivation(id, work) {
|
|
390
703
|
const previous = this.#activationTails.get(id) ?? Promise.resolve();
|
|
391
704
|
let finish = () => undefined;
|
|
392
705
|
const settled = new Promise((resolve) => {
|
|
@@ -404,108 +717,136 @@ export class SpineServices {
|
|
|
404
717
|
this.#activationTails.delete(id);
|
|
405
718
|
}
|
|
406
719
|
}
|
|
407
|
-
|
|
720
|
+
/**
|
|
721
|
+
* Leaves an absent subscription ID inert and checks known records before removal.
|
|
722
|
+
*
|
|
723
|
+
* @param subscription Subscription identity supplied by the caller.
|
|
724
|
+
* @returns Cancellation response after any required membership check.
|
|
725
|
+
* @param reader The verified reader facts for this operation.
|
|
726
|
+
*/
|
|
727
|
+
#cancel(subscription, reader) {
|
|
728
|
+
ReadRequestStatus.check("subscription", () => Validate.check(SubscriptionSchema, subscription));
|
|
729
|
+
ReadRequestStatus.checkLifecycleCriterion(subscription);
|
|
408
730
|
const id = subscription.id?.value;
|
|
409
731
|
if (id === undefined) {
|
|
410
732
|
return ServiceValues.okResponse();
|
|
411
733
|
}
|
|
412
|
-
return this.#
|
|
734
|
+
return this.#cancelKnown(id, reader);
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Checks the canonical record's tenant before cancellation side effects.
|
|
738
|
+
*
|
|
739
|
+
* @param id Subscription ID with a possible shared-registry record.
|
|
740
|
+
* @returns Successful cancellation response after removal.
|
|
741
|
+
* @param reader The verified reader facts for this operation.
|
|
742
|
+
*/
|
|
743
|
+
async #cancelKnown(id, reader) {
|
|
744
|
+
const live = this.#subscriptions.get(id);
|
|
745
|
+
const record = live ?? (await this.#findSubscription(id));
|
|
746
|
+
if (record === undefined)
|
|
747
|
+
return ServiceValues.okResponse();
|
|
748
|
+
if (!ReaderIdentities.matches(record.reader, reader))
|
|
749
|
+
throw this.#readerDenied();
|
|
750
|
+
await this.#requireSubscriptionTenant(record, async () => {
|
|
751
|
+
if (live !== undefined)
|
|
752
|
+
return this.#removeSubscription(id, reader);
|
|
753
|
+
await this.#registryOperation(record.route.context, async () => {
|
|
754
|
+
const outcome = await boundedContextAccess
|
|
755
|
+
.subscriptionRegistry(record.route.context)
|
|
756
|
+
.delete(create(SubscriptionIdSchema, { value: id }), reader);
|
|
757
|
+
if (outcome === "denied")
|
|
758
|
+
throw this.#readerDenied();
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
return ServiceValues.okResponse();
|
|
413
762
|
}
|
|
414
|
-
|
|
763
|
+
/**
|
|
764
|
+
* Returns a sanitized denial for a foreign or legacy-bound subscription.
|
|
765
|
+
*
|
|
766
|
+
* @returns The reader authorization error.
|
|
767
|
+
*/
|
|
768
|
+
#readerDenied() {
|
|
769
|
+
return new ConnectError("Subscription reader is not authorized.", Code.PermissionDenied);
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Checks the discovered subscription tenant before tenant-specific control work.
|
|
773
|
+
*
|
|
774
|
+
* @param record Canonical shared-registry subscription record.
|
|
775
|
+
* @param onReady Complete admitted one-shot mutation, if present.
|
|
776
|
+
* @returns Completion after the operation settles.
|
|
777
|
+
*/
|
|
778
|
+
async #requireSubscriptionTenant(record, onReady) {
|
|
779
|
+
await TenantStatuses.require(record.route.context, record.tenantId, onReady);
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Finds the registered state or event route for a Topic target.
|
|
783
|
+
*
|
|
784
|
+
* @param topic Framework subscription Topic.
|
|
785
|
+
* @returns The matching subscription route.
|
|
786
|
+
*/ #subscriptionRoute(topic) {
|
|
415
787
|
const target = topic.target;
|
|
416
788
|
const route = target === undefined
|
|
417
789
|
? undefined
|
|
418
790
|
: (this.#stateRoutes.get(target.type) ?? this.#eventRoutes.get(target.type));
|
|
419
791
|
if (target === undefined || route === undefined) {
|
|
420
|
-
throw
|
|
792
|
+
throw ReadRequestStatus.invalid("topic", true, "Unsupported subscription target.");
|
|
421
793
|
}
|
|
422
794
|
return route;
|
|
423
795
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
record.delivery.
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
if (!(error instanceof TypeError) ||
|
|
442
|
-
(!error.message.includes("requires a Stand instance") &&
|
|
443
|
-
!error.message.includes("requires a built BoundedContext instance"))) {
|
|
444
|
-
throw error;
|
|
445
|
-
}
|
|
446
|
-
this.#createSubscriptionAttachment(record);
|
|
447
|
-
}
|
|
796
|
+
/**
|
|
797
|
+
* Attaches delivery for the validated retained subscription.
|
|
798
|
+
*
|
|
799
|
+
* @param record The durable subscription record.
|
|
800
|
+
* @returns Completion after the operation settles.
|
|
801
|
+
* @param expected Exact entry activated for this reader.
|
|
802
|
+
*/ async #activateRecord(record, expected) {
|
|
803
|
+
const consumer = await boundedContextAccess.consumeSubscription(record.route.context, record.id, (work) => {
|
|
804
|
+
record.delivery.push(work);
|
|
805
|
+
if (record.delivery.closed)
|
|
806
|
+
this.#removeDetachedSubscription(record.id);
|
|
807
|
+
}, () => {
|
|
808
|
+
if (this.#subscriptions.get(record.id) === record)
|
|
809
|
+
this.#subscriptions.delete(record.id);
|
|
810
|
+
record.delivery.invalidate();
|
|
811
|
+
}, expected);
|
|
812
|
+
record.delivery.attach(consumer);
|
|
448
813
|
managedChildSubscriptionAccess.installed(record.id);
|
|
449
814
|
}
|
|
450
|
-
#createSubscriptionAttachment(record) {
|
|
451
|
-
if (record.kind === "event")
|
|
452
|
-
return this.#attachEventSubscription(record);
|
|
453
|
-
const stateRecord = record;
|
|
454
|
-
const standSubscription = stateRecord.route.context.stand().subscribe(stateRecord.route.schema, (update) => {
|
|
455
|
-
const subscriptionUpdate = ServiceValues.createEntityUpdate(stateRecord, update);
|
|
456
|
-
if (subscriptionUpdate !== undefined) {
|
|
457
|
-
stateRecord.delivery.push(subscriptionUpdate);
|
|
458
|
-
}
|
|
459
|
-
if (stateRecord.delivery.closed) {
|
|
460
|
-
this.#removeDetachedSubscription(stateRecord.id);
|
|
461
|
-
}
|
|
462
|
-
}, ServiceValues.tenantOptions(stateRecord.tenantId));
|
|
463
|
-
stateRecord.delivery.attach(standSubscription);
|
|
464
|
-
return standSubscription;
|
|
465
|
-
}
|
|
466
815
|
/**
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
* @
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
onEvent: (event) => {
|
|
474
|
-
if (!ServiceValues.eventTenantMatches(record, event))
|
|
475
|
-
return;
|
|
476
|
-
if (record.eventMatcher !== undefined && !record.eventMatcher(event))
|
|
477
|
-
return;
|
|
478
|
-
record.delivery.push(ServiceValues.createEventUpdate(record, event));
|
|
479
|
-
if (record.delivery.closed)
|
|
480
|
-
this.#removeDetachedSubscription(record.id);
|
|
481
|
-
},
|
|
482
|
-
});
|
|
483
|
-
record.delivery.attach(eventSubscription);
|
|
484
|
-
return eventSubscription;
|
|
485
|
-
}
|
|
486
|
-
#removeSubscription(id) {
|
|
816
|
+
* Serializes removal of a retained subscription and its delivery.
|
|
817
|
+
*
|
|
818
|
+
* @param id The stable identifier for this record.
|
|
819
|
+
* @param reader The verified reader facts for this operation.
|
|
820
|
+
* @returns Completion after the operation settles.
|
|
821
|
+
*/ #removeSubscription(id, reader) {
|
|
487
822
|
const existing = this.#removals.get(id);
|
|
488
823
|
if (existing !== undefined) {
|
|
489
824
|
return existing.settled;
|
|
490
825
|
}
|
|
491
826
|
const record = this.#subscriptions.get(id);
|
|
492
827
|
const unknown = record === undefined;
|
|
828
|
+
if (reader !== undefined && unknown)
|
|
829
|
+
return Promise.resolve();
|
|
493
830
|
if (unknown && this.#unknownRemovals.size >= this.#subscriptionLimit) {
|
|
494
831
|
return Promise.reject(new ConnectError("Subscription cancellation capacity is exhausted.", Code.ResourceExhausted));
|
|
495
832
|
}
|
|
496
833
|
if (unknown) {
|
|
497
834
|
this.#unknownRemovals.add(id);
|
|
498
835
|
}
|
|
499
|
-
else {
|
|
836
|
+
else if (reader === undefined) {
|
|
500
837
|
record.delivery.close();
|
|
501
838
|
this.#subscriptions.delete(id);
|
|
502
839
|
}
|
|
503
840
|
const removal = ServiceValues.createSubscriptionRemoval();
|
|
504
841
|
this.#removals.set(id, removal);
|
|
505
|
-
void this.#runRemoval(id, removal, record, unknown);
|
|
842
|
+
void this.#runRemoval(id, removal, record, unknown, reader);
|
|
506
843
|
return removal.settled;
|
|
507
844
|
}
|
|
508
|
-
|
|
845
|
+
/**
|
|
846
|
+
* Cleans up delivery after a subscription record is detached.
|
|
847
|
+
*
|
|
848
|
+
* @param id The stable identifier for this record.
|
|
849
|
+
*/ #removeDetachedSubscription(id) {
|
|
509
850
|
// spine-log-boundary: server.subscription_cleanup_failure
|
|
510
851
|
void this.#removeSubscription(id).catch(() => {
|
|
511
852
|
const logger = serviceLoggers.get(this);
|
|
@@ -518,18 +859,30 @@ export class SpineServices {
|
|
|
518
859
|
}
|
|
519
860
|
});
|
|
520
861
|
}
|
|
521
|
-
|
|
862
|
+
/**
|
|
863
|
+
* Executes serialized subscription cleanup for one ID.
|
|
864
|
+
*
|
|
865
|
+
* @param id The stable identifier for this record.
|
|
866
|
+
* @param removal Shared subscription-removal task.
|
|
867
|
+
* @param record The durable subscription record.
|
|
868
|
+
* @param unknown Unrecognized route or filter value.
|
|
869
|
+
* @param reader The verified reader facts for this operation.
|
|
870
|
+
* @returns Completion after the operation settles.
|
|
871
|
+
*/ async #runRemoval(id, removal, record, unknown, reader) {
|
|
522
872
|
try {
|
|
523
873
|
if (record !== undefined) {
|
|
524
|
-
|
|
874
|
+
const deleted = await boundedContextAccess
|
|
875
|
+
.subscriptionRegistry(record.route.context)
|
|
876
|
+
.delete(create(SubscriptionIdSchema, { value: id }), reader);
|
|
877
|
+
if (deleted === "denied")
|
|
878
|
+
throw this.#readerDenied();
|
|
879
|
+
if (reader !== undefined) {
|
|
880
|
+
record.delivery.close();
|
|
881
|
+
this.#subscriptions.delete(id);
|
|
882
|
+
}
|
|
525
883
|
}
|
|
526
884
|
else {
|
|
527
|
-
await
|
|
528
|
-
const registry = this.#subscriptionRegistry(context);
|
|
529
|
-
return registry === undefined
|
|
530
|
-
? []
|
|
531
|
-
: [registry.delete(create(SubscriptionIdSchema, { value: id }))];
|
|
532
|
-
}));
|
|
885
|
+
await this.#deleteUnknownSubscription(id);
|
|
533
886
|
}
|
|
534
887
|
removal.resolve();
|
|
535
888
|
}
|
|
@@ -545,10 +898,31 @@ export class SpineServices {
|
|
|
545
898
|
}
|
|
546
899
|
}
|
|
547
900
|
}
|
|
548
|
-
|
|
901
|
+
/**
|
|
902
|
+
* Deletes an unknown subscription from every configured context registry.
|
|
903
|
+
*
|
|
904
|
+
* @param id Subscription ID to remove.
|
|
905
|
+
* @returns Completion after the operation settles.
|
|
906
|
+
*/
|
|
907
|
+
async #deleteUnknownSubscription(id) {
|
|
908
|
+
await Promise.all(ServiceValues.uniqueContexts(this.#contexts).map((context) => this.#registryOperation(context, async () => {
|
|
909
|
+
await boundedContextAccess
|
|
910
|
+
.subscriptionRegistry(context)
|
|
911
|
+
.delete(create(SubscriptionIdSchema, { value: id }));
|
|
912
|
+
})));
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Loads a retained subscription record by opaque ID.
|
|
916
|
+
*
|
|
917
|
+
* @param id The stable identifier for this record.
|
|
918
|
+
* @returns The retained record, if present.
|
|
919
|
+
*/ async #findSubscription(id) {
|
|
549
920
|
const subscriptionId = create(SubscriptionIdSchema, { value: id });
|
|
550
921
|
for (const context of ServiceValues.uniqueContexts(this.#contexts)) {
|
|
551
|
-
|
|
922
|
+
let entry;
|
|
923
|
+
await this.#registryOperation(context, async () => {
|
|
924
|
+
entry = await boundedContextAccess.subscriptionRegistry(context).get(subscriptionId);
|
|
925
|
+
});
|
|
552
926
|
const subscription = entry?.subscription;
|
|
553
927
|
const topic = subscription?.topic;
|
|
554
928
|
if (entry === undefined || subscription === undefined || topic === undefined)
|
|
@@ -566,37 +940,52 @@ export class SpineServices {
|
|
|
566
940
|
subscription: clone(SubscriptionSchema, subscription),
|
|
567
941
|
shape: ServiceValues.createSubscriptionShape(canonicalTopic, route),
|
|
568
942
|
tenantId,
|
|
569
|
-
|
|
943
|
+
queueCapacity: this.#queueCapacity,
|
|
944
|
+
...(entry.reader === undefined ? {} : { reader: entry.reader }),
|
|
570
945
|
});
|
|
571
946
|
}
|
|
572
947
|
return undefined;
|
|
573
948
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
// contexts always provide their configured registry through the access seam.
|
|
584
|
-
const key = context;
|
|
585
|
-
let registry = this.#testRegistries.get(key);
|
|
586
|
-
if (registry === undefined) {
|
|
587
|
-
registry = new InMemorySubscriptionRegistry();
|
|
588
|
-
this.#testRegistries.set(key, registry);
|
|
589
|
-
}
|
|
590
|
-
return registry;
|
|
591
|
-
}
|
|
949
|
+
/**
|
|
950
|
+
* Retains one short registry read or delete through the context close drain.
|
|
951
|
+
*
|
|
952
|
+
* @param context Context selected by the shared subscription route.
|
|
953
|
+
* @param onOperation Complete registry callback.
|
|
954
|
+
* @returns Completion before the selected registry may close.
|
|
955
|
+
*/
|
|
956
|
+
#registryOperation(context, onOperation) {
|
|
957
|
+
return boundedContextAccess.admitServiceRegistryOperation(context, onOperation);
|
|
592
958
|
}
|
|
593
959
|
}
|
|
960
|
+
_a = SpineServices;
|
|
594
961
|
/**
|
|
595
962
|
* Provides package-private service configuration seams.
|
|
596
963
|
*
|
|
597
964
|
* @internal
|
|
598
965
|
*/
|
|
599
966
|
export const spineServicesAccess = Object.freeze({
|
|
967
|
+
/**
|
|
968
|
+
* Attaches Server-only outward read policies to its service adapter.
|
|
969
|
+
*
|
|
970
|
+
* @param services Prepared service adapter.
|
|
971
|
+
* @param filters Registered policy engine.
|
|
972
|
+
*/
|
|
973
|
+
installReadFilters(services, filters) {
|
|
974
|
+
if (!serviceInstances.has(services))
|
|
975
|
+
throw new TypeError("Expected SpineServices instance.");
|
|
976
|
+
serviceReadFilters.set(services, filters);
|
|
977
|
+
},
|
|
978
|
+
/**
|
|
979
|
+
* Attaches this Server's prepared handler table without exposing registration options.
|
|
980
|
+
*
|
|
981
|
+
* @param services Adapter receiving its internal App Query capability.
|
|
982
|
+
* @param queries Selected handler table from Server preparation.
|
|
983
|
+
*/
|
|
984
|
+
installAppQueries(services, queries) {
|
|
985
|
+
if (!serviceInstances.has(services))
|
|
986
|
+
throw new TypeError("Expected SpineServices instance.");
|
|
987
|
+
serviceAppQueries.set(services, queries);
|
|
988
|
+
},
|
|
600
989
|
/**
|
|
601
990
|
* Clears the logger installed for a closed service adapter.
|
|
602
991
|
*
|
|
@@ -625,18 +1014,23 @@ export const spineServicesAccess = Object.freeze({
|
|
|
625
1014
|
* Buffers subscription updates and wakes active stream readers.
|
|
626
1015
|
*/
|
|
627
1016
|
class SubscriptionDelivery {
|
|
1017
|
+
#active;
|
|
628
1018
|
#queue = [];
|
|
629
1019
|
#waiters = [];
|
|
630
|
-
#
|
|
1020
|
+
#controller = new AbortController();
|
|
1021
|
+
#queueCapacity;
|
|
1022
|
+
#activationSignal;
|
|
1023
|
+
#onActivationAbort;
|
|
631
1024
|
#subscription;
|
|
632
1025
|
#closed = false;
|
|
1026
|
+
#invalidated = false;
|
|
633
1027
|
/**
|
|
634
1028
|
* Sets the bounded update queue capacity.
|
|
635
1029
|
*
|
|
636
|
-
* @param
|
|
1030
|
+
* @param queueCapacity Maximum waiting updates beyond the active update.
|
|
637
1031
|
*/
|
|
638
|
-
constructor(
|
|
639
|
-
this.#
|
|
1032
|
+
constructor(queueCapacity) {
|
|
1033
|
+
this.#queueCapacity = queueCapacity;
|
|
640
1034
|
}
|
|
641
1035
|
/**
|
|
642
1036
|
* Checks whether a source attachment is active.
|
|
@@ -654,6 +1048,75 @@ class SubscriptionDelivery {
|
|
|
654
1048
|
get closed() {
|
|
655
1049
|
return this.#closed;
|
|
656
1050
|
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Returns source invalidation separately from transport cancellation before the first ACK.
|
|
1053
|
+
* @returns Whether the admitted canonical entry changed or disappeared.
|
|
1054
|
+
*/
|
|
1055
|
+
get invalidated() {
|
|
1056
|
+
return this.#invalidated;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Closes this delivery after the admitted source entry changes.
|
|
1060
|
+
*/
|
|
1061
|
+
invalidate() {
|
|
1062
|
+
if (this.#closed)
|
|
1063
|
+
return;
|
|
1064
|
+
this.#invalidated = true;
|
|
1065
|
+
this.close();
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Returns a cancellation signal for active policy when this consumer is removed.
|
|
1069
|
+
*
|
|
1070
|
+
* @returns Cancellation signal for the current activation.
|
|
1071
|
+
*/
|
|
1072
|
+
get signal() {
|
|
1073
|
+
return this.#controller.signal;
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Registers transport cancellation before the source attachment awaits.
|
|
1077
|
+
*
|
|
1078
|
+
* @param signal Cancellation of the admitted activation, when available.
|
|
1079
|
+
*/
|
|
1080
|
+
track(signal) {
|
|
1081
|
+
if (signal === undefined)
|
|
1082
|
+
return;
|
|
1083
|
+
if (signal.aborted) {
|
|
1084
|
+
this.close();
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
this.#activationSignal = signal;
|
|
1088
|
+
this.#onActivationAbort = () => {
|
|
1089
|
+
this.close();
|
|
1090
|
+
};
|
|
1091
|
+
signal.addEventListener("abort", this.#onActivationAbort, { once: true });
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Stops waiting on one policy call when this activation closes.
|
|
1095
|
+
*
|
|
1096
|
+
* @param work The policy result; a late rejection remains observed by the race.
|
|
1097
|
+
* @returns The result, or undefined after cancellation.
|
|
1098
|
+
*/
|
|
1099
|
+
async settle(work) {
|
|
1100
|
+
if (this.#closed) {
|
|
1101
|
+
// spine-log-boundary: server.subscription_closed_policy_rejection
|
|
1102
|
+
void work.catch(() => undefined);
|
|
1103
|
+
return undefined;
|
|
1104
|
+
}
|
|
1105
|
+
let onAbort;
|
|
1106
|
+
const closed = new Promise((resolve) => {
|
|
1107
|
+
onAbort = () => {
|
|
1108
|
+
resolve(undefined);
|
|
1109
|
+
};
|
|
1110
|
+
this.#controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
1111
|
+
});
|
|
1112
|
+
try {
|
|
1113
|
+
return await Promise.race([work, closed]);
|
|
1114
|
+
}
|
|
1115
|
+
finally {
|
|
1116
|
+
if (onAbort !== undefined)
|
|
1117
|
+
this.#controller.signal.removeEventListener("abort", onAbort);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
657
1120
|
/**
|
|
658
1121
|
* Attaches a source subscription or immediately removes it after close.
|
|
659
1122
|
*
|
|
@@ -675,17 +1138,16 @@ class SubscriptionDelivery {
|
|
|
675
1138
|
if (this.#closed) {
|
|
676
1139
|
return;
|
|
677
1140
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
this.#queue.push(update);
|
|
1141
|
+
if (this.#active === undefined) {
|
|
1142
|
+
this.#active = update;
|
|
1143
|
+
this.#waiters.shift()?.(update);
|
|
1144
|
+
return;
|
|
685
1145
|
}
|
|
686
|
-
|
|
687
|
-
|
|
1146
|
+
if (this.#queue.length >= this.#queueCapacity) {
|
|
1147
|
+
this.close();
|
|
1148
|
+
return;
|
|
688
1149
|
}
|
|
1150
|
+
this.#queue.push(update);
|
|
689
1151
|
}
|
|
690
1152
|
/**
|
|
691
1153
|
* Reads the next queued update, waiting while the stream is open and empty.
|
|
@@ -693,12 +1155,22 @@ class SubscriptionDelivery {
|
|
|
693
1155
|
* @returns Next update, or `undefined` after closure.
|
|
694
1156
|
*/
|
|
695
1157
|
next() {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
1158
|
+
if (this.#closed)
|
|
1159
|
+
return Promise.resolve(undefined);
|
|
1160
|
+
if (this.#active !== undefined)
|
|
1161
|
+
return Promise.resolve(this.#active);
|
|
700
1162
|
return new Promise((resolve) => this.#waiters.push(resolve));
|
|
701
1163
|
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Removes the consumed active update and promotes the next waiting update.
|
|
1166
|
+
*/
|
|
1167
|
+
advance() {
|
|
1168
|
+
if (this.#closed)
|
|
1169
|
+
return;
|
|
1170
|
+
this.#active = this.#queue.shift();
|
|
1171
|
+
if (this.#active !== undefined)
|
|
1172
|
+
this.#waiters.shift()?.(this.#active);
|
|
1173
|
+
}
|
|
702
1174
|
/**
|
|
703
1175
|
* Closes the source subscription and completes all waiting readers.
|
|
704
1176
|
*/
|
|
@@ -707,7 +1179,11 @@ class SubscriptionDelivery {
|
|
|
707
1179
|
return;
|
|
708
1180
|
}
|
|
709
1181
|
this.#closed = true;
|
|
1182
|
+
this.#controller.abort();
|
|
1183
|
+
if (this.#activationSignal !== undefined && this.#onActivationAbort !== undefined)
|
|
1184
|
+
this.#activationSignal.removeEventListener("abort", this.#onActivationAbort);
|
|
710
1185
|
this.#queue.length = 0;
|
|
1186
|
+
this.#active = undefined;
|
|
711
1187
|
this.#subscription?.unsubscribe();
|
|
712
1188
|
this.#subscription = undefined;
|
|
713
1189
|
for (const waiter of this.#waiters.splice(0)) {
|
|
@@ -717,6 +1193,8 @@ class SubscriptionDelivery {
|
|
|
717
1193
|
}
|
|
718
1194
|
/**
|
|
719
1195
|
* Builds an OK status for service responses.
|
|
1196
|
+
*
|
|
1197
|
+
* @returns The successful status.
|
|
720
1198
|
*/
|
|
721
1199
|
function okStatus() {
|
|
722
1200
|
return create(StatusSchema, {
|
|
@@ -726,30 +1204,42 @@ function okStatus() {
|
|
|
726
1204
|
},
|
|
727
1205
|
});
|
|
728
1206
|
}
|
|
729
|
-
|
|
1207
|
+
/**
|
|
1208
|
+
* Builds a structured status from a public validation error.
|
|
1209
|
+
*
|
|
1210
|
+
* @param type Registered target type URL.
|
|
1211
|
+
* @param message Public validation or error message.
|
|
1212
|
+
* @param details Structured validation details.
|
|
1213
|
+
* @param code Public status code.
|
|
1214
|
+
* @returns The structured error status.
|
|
1215
|
+
*/ function errorStatus(type, message, details, code) {
|
|
730
1216
|
return create(StatusSchema, {
|
|
731
1217
|
status: {
|
|
732
1218
|
case: "error",
|
|
733
1219
|
value: create(ErrorSchema, {
|
|
734
1220
|
type,
|
|
735
1221
|
message,
|
|
1222
|
+
...(code === undefined ? {} : { code }),
|
|
736
1223
|
...(details === undefined ? {} : { details }),
|
|
737
1224
|
}),
|
|
738
1225
|
},
|
|
739
1226
|
});
|
|
740
1227
|
}
|
|
741
|
-
|
|
1228
|
+
/**
|
|
1229
|
+
* Builds a successful subscription service response.
|
|
1230
|
+
*
|
|
1231
|
+
* @returns The OK response.
|
|
1232
|
+
*/ function okResponse() {
|
|
742
1233
|
return create(ResponseSchema, { status: okStatus() });
|
|
743
1234
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
function validateReadQuery(query, target, route) {
|
|
1235
|
+
/**
|
|
1236
|
+
* Checks a read query envelope and target before routing.
|
|
1237
|
+
*
|
|
1238
|
+
* @param query Framework Entity read query.
|
|
1239
|
+
* @param target Declared query or Topic target.
|
|
1240
|
+
* @param route Registered target route with schema and tenancy.
|
|
1241
|
+
* @returns The validated read request.
|
|
1242
|
+
*/ function validateReadQuery(query, target, route) {
|
|
753
1243
|
const formatError = formatReadError(query.format, route);
|
|
754
1244
|
if (formatError !== undefined) {
|
|
755
1245
|
return formatError;
|
|
@@ -766,7 +1256,13 @@ function validateReadQuery(query, target, route) {
|
|
|
766
1256
|
return invalidCriterionError();
|
|
767
1257
|
}
|
|
768
1258
|
}
|
|
769
|
-
|
|
1259
|
+
/**
|
|
1260
|
+
* Checks target filters against the registered Entity schema.
|
|
1261
|
+
*
|
|
1262
|
+
* @param filters Target or subscription filter collection.
|
|
1263
|
+
* @param route Registered target route with schema and tenancy.
|
|
1264
|
+
* @returns The validated target filters.
|
|
1265
|
+
*/ function validateFilters(filters, route) {
|
|
770
1266
|
const idCount = filters.idFilter?.id.length ?? 0;
|
|
771
1267
|
if (idCount > QueryLimits.idCount) {
|
|
772
1268
|
return invalidQueryError("QueryService.Read id_filter may contain at most 100 IDs.");
|
|
@@ -793,7 +1289,13 @@ function validateFilters(filters, route) {
|
|
|
793
1289
|
}
|
|
794
1290
|
: undefined;
|
|
795
1291
|
}
|
|
796
|
-
|
|
1292
|
+
/**
|
|
1293
|
+
* Checks nested composite filters and their traversal bounds.
|
|
1294
|
+
*
|
|
1295
|
+
* @param filters Target or subscription filter collection.
|
|
1296
|
+
* @param route Registered target route with schema and tenancy.
|
|
1297
|
+
* @returns The validated composite filters.
|
|
1298
|
+
*/ function validateCompositeFilters(filters, route) {
|
|
797
1299
|
const pending = filters.map((filter) => ({ filter, depth: 0 }));
|
|
798
1300
|
const seen = new WeakSet();
|
|
799
1301
|
let compositeCount = 0;
|
|
@@ -817,17 +1319,29 @@ function validateCompositeFilters(filters, route) {
|
|
|
817
1319
|
}
|
|
818
1320
|
return undefined;
|
|
819
1321
|
}
|
|
820
|
-
|
|
1322
|
+
/**
|
|
1323
|
+
* Checks query ID Any payloads against the registered Entity ID.
|
|
1324
|
+
*
|
|
1325
|
+
* @param ids Opaque query or subscription IDs.
|
|
1326
|
+
* @param route Registered target route with schema and tenancy.
|
|
1327
|
+
* @returns The validated query IDs.
|
|
1328
|
+
*/ function validateQueryIds(ids, route) {
|
|
821
1329
|
const schema = route.idField.message;
|
|
822
1330
|
if (schema === undefined || ids === undefined) {
|
|
823
1331
|
return undefined;
|
|
824
1332
|
}
|
|
825
|
-
if (ids.some((id) => id === undefined ||
|
|
1333
|
+
if (ids.some((id) => id === undefined ||
|
|
1334
|
+
(schema.typeName !== AnySchema.typeName && AnyPacker.tryUnpack(id, schema) === undefined))) {
|
|
826
1335
|
return invalidQueryError(`QueryService.Read id_filter values must pack ${schema.typeName}.`);
|
|
827
1336
|
}
|
|
828
1337
|
return undefined;
|
|
829
1338
|
}
|
|
830
|
-
|
|
1339
|
+
/**
|
|
1340
|
+
* Counts leaf filters in a nested subscription criterion.
|
|
1341
|
+
*
|
|
1342
|
+
* @param filters Target or subscription filter collection.
|
|
1343
|
+
* @returns The number of simple filter nodes.
|
|
1344
|
+
*/ function countSimpleFilters(filters) {
|
|
831
1345
|
const pending = [...filters];
|
|
832
1346
|
let count = 0;
|
|
833
1347
|
while (pending.length > 0) {
|
|
@@ -839,7 +1353,13 @@ function countSimpleFilters(filters) {
|
|
|
839
1353
|
}
|
|
840
1354
|
return count;
|
|
841
1355
|
}
|
|
842
|
-
|
|
1356
|
+
/**
|
|
1357
|
+
* Checks field filters and their typed Any values.
|
|
1358
|
+
*
|
|
1359
|
+
* @param filters Target or subscription filter collection.
|
|
1360
|
+
* @param route Registered target route with schema and tenancy.
|
|
1361
|
+
* @returns The validated simple filters.
|
|
1362
|
+
*/ function validateSimpleFilters(filters, route) {
|
|
843
1363
|
for (const filter of filters) {
|
|
844
1364
|
if (wireComparison(filter.operator) === undefined) {
|
|
845
1365
|
return unsupportedFilterError("QueryService.Read comparison operator is not supported.");
|
|
@@ -872,6 +1392,10 @@ function validateSimpleFilters(filters, route) {
|
|
|
872
1392
|
}
|
|
873
1393
|
/**
|
|
874
1394
|
* Checks query ordering and limits against the registered state route.
|
|
1395
|
+
*
|
|
1396
|
+
* @param format Declared filter value format.
|
|
1397
|
+
* @param route Registered target route with schema and tenancy.
|
|
1398
|
+
* @returns The structured read error.
|
|
875
1399
|
*/
|
|
876
1400
|
function formatReadError(format, route) {
|
|
877
1401
|
if (format === undefined) {
|
|
@@ -896,6 +1420,11 @@ function formatReadError(format, route) {
|
|
|
896
1420
|
}
|
|
897
1421
|
/**
|
|
898
1422
|
* Converts an accepted wire query into a storage plan for complete states.
|
|
1423
|
+
*
|
|
1424
|
+
* @param target Declared query or Topic target.
|
|
1425
|
+
* @param query Framework Entity read query.
|
|
1426
|
+
* @param route Registered target route with schema and tenancy.
|
|
1427
|
+
* @returns The validated Entity read plan.
|
|
899
1428
|
*/
|
|
900
1429
|
function createReadPlan(target, query, route) {
|
|
901
1430
|
const predicate = target.criterion.case === "filters"
|
|
@@ -917,7 +1446,13 @@ function createReadPlan(target, query, route) {
|
|
|
917
1446
|
...(format === undefined || format.limit === 0 ? {} : { limit: format.limit }),
|
|
918
1447
|
});
|
|
919
1448
|
}
|
|
920
|
-
|
|
1449
|
+
/**
|
|
1450
|
+
* Converts validated target filters to executable predicates.
|
|
1451
|
+
*
|
|
1452
|
+
* @param filters Target or subscription filter collection.
|
|
1453
|
+
* @param route Registered target route with schema and tenancy.
|
|
1454
|
+
* @returns The normalized filter predicates.
|
|
1455
|
+
*/ function normalizedFilters(filters, route) {
|
|
921
1456
|
const predicates = [];
|
|
922
1457
|
if (filters.idFilter !== undefined) {
|
|
923
1458
|
predicates.push({
|
|
@@ -931,11 +1466,23 @@ function normalizedFilters(filters, route) {
|
|
|
931
1466
|
? onlyPredicate
|
|
932
1467
|
: { kind: "all", predicates };
|
|
933
1468
|
}
|
|
934
|
-
|
|
1469
|
+
/**
|
|
1470
|
+
* Decodes an opaque query ID using the registered Entity ID schema.
|
|
1471
|
+
*
|
|
1472
|
+
* @param value Packed Entity ID Any from the query.
|
|
1473
|
+
* @param route Registered target route with schema and tenancy.
|
|
1474
|
+
* @returns The typed Entity ID.
|
|
1475
|
+
*/ function decodeQueryIdValue(value, route) {
|
|
935
1476
|
const schema = route.idField.message;
|
|
936
|
-
return schema === undefined ? decodeAnyValue(value) :
|
|
1477
|
+
return schema === undefined ? decodeAnyValue(value) : decodeAnyValue(value, schema);
|
|
937
1478
|
}
|
|
938
|
-
|
|
1479
|
+
/**
|
|
1480
|
+
* Converts a validated composite filter to its execution form.
|
|
1481
|
+
*
|
|
1482
|
+
* @param filter One target or subscription filter.
|
|
1483
|
+
* @param route Registered target route with schema and tenancy.
|
|
1484
|
+
* @returns The normalized composite criterion.
|
|
1485
|
+
*/ function normalizedComposite(filter, route) {
|
|
939
1486
|
const predicates = [
|
|
940
1487
|
...filter.filter.map((child) => ({
|
|
941
1488
|
kind: "comparison",
|
|
@@ -950,28 +1497,44 @@ function normalizedComposite(filter, route) {
|
|
|
950
1497
|
predicates,
|
|
951
1498
|
};
|
|
952
1499
|
}
|
|
953
|
-
|
|
1500
|
+
/**
|
|
1501
|
+
* Resolves a declared state column for a field filter.
|
|
1502
|
+
*
|
|
1503
|
+
* @param filter One target or subscription filter.
|
|
1504
|
+
* @returns The required state column.
|
|
1505
|
+
*/ function requiredFilterColumn(filter) {
|
|
954
1506
|
const column = filter.fieldPath?.fieldName[0];
|
|
955
1507
|
if (column !== undefined)
|
|
956
1508
|
return column;
|
|
957
1509
|
throw new TypeError("Validated query filter has no column.");
|
|
958
1510
|
}
|
|
959
|
-
|
|
1511
|
+
/**
|
|
1512
|
+
* Maps a wire filter operator to a supported comparison.
|
|
1513
|
+
*
|
|
1514
|
+
* @param filter One target or subscription filter.
|
|
1515
|
+
* @returns The required comparison operator.
|
|
1516
|
+
*/ function requiredComparison(filter) {
|
|
960
1517
|
const operator = wireComparison(filter.operator);
|
|
961
1518
|
if (operator !== undefined)
|
|
962
1519
|
return operator;
|
|
963
1520
|
throw new TypeError("Validated query filter has no supported comparison operator.");
|
|
964
1521
|
}
|
|
965
|
-
|
|
1522
|
+
/**
|
|
1523
|
+
* Decodes a filter value for the target state column.
|
|
1524
|
+
*
|
|
1525
|
+
* @param filter One target or subscription filter.
|
|
1526
|
+
* @param route Registered target route with schema and tenancy.
|
|
1527
|
+
* @returns The typed column value.
|
|
1528
|
+
*/ function decodeColumnValue(filter, route) {
|
|
966
1529
|
const column = filter.fieldPath?.fieldName[0] ?? "";
|
|
967
1530
|
if (column === "version" && filter.value !== undefined) {
|
|
968
|
-
const value =
|
|
1531
|
+
const value = AnyPacker.tryUnpack(filter.value, VersionSchema);
|
|
969
1532
|
if (value !== undefined)
|
|
970
1533
|
return value;
|
|
971
1534
|
throw new TypeError('Validated query column "version" has an invalid value.');
|
|
972
1535
|
}
|
|
973
1536
|
if ((column === "archived" || column === "deleted") && filter.value !== undefined) {
|
|
974
|
-
const value =
|
|
1537
|
+
const value = AnyPacker.tryUnpack(filter.value, BoolValueSchema);
|
|
975
1538
|
if (value !== undefined)
|
|
976
1539
|
return value.value;
|
|
977
1540
|
throw new TypeError(`Validated query column "${column}" has an invalid value.`);
|
|
@@ -979,9 +1542,16 @@ function decodeColumnValue(filter, route) {
|
|
|
979
1542
|
const field = route.columnFields.get(column);
|
|
980
1543
|
return decodeAnyValue(filter.value, field?.fieldKind === "message" ? field.message : undefined);
|
|
981
1544
|
}
|
|
982
|
-
|
|
1545
|
+
/**
|
|
1546
|
+
* Checks an Any filter value against the registered field schema.
|
|
1547
|
+
*
|
|
1548
|
+
* @param field Registered Protobuf field descriptor.
|
|
1549
|
+
* @param value Packed field-filter value.
|
|
1550
|
+
* @returns Whether the filter value has the expected type.
|
|
1551
|
+
*/ function filterValueMatches(field, value) {
|
|
983
1552
|
const schema = filterValueSchema(field);
|
|
984
|
-
if (schema === undefined ||
|
|
1553
|
+
if (schema === undefined ||
|
|
1554
|
+
(schema.typeName !== AnySchema.typeName && value.typeUrl !== TypeUrls.derive(schema)))
|
|
985
1555
|
return false;
|
|
986
1556
|
const decoded = field.fieldKind === "message" ? decodeAnyValue(value, schema) : decodeAnyValue(value);
|
|
987
1557
|
if (field.fieldKind === "message") {
|
|
@@ -1006,7 +1576,12 @@ function filterValueMatches(field, value) {
|
|
|
1006
1576
|
}
|
|
1007
1577
|
return typeof decoded === "number" && Number.isFinite(decoded);
|
|
1008
1578
|
}
|
|
1009
|
-
|
|
1579
|
+
/**
|
|
1580
|
+
* Selects the Protobuf schema required for a state field filter.
|
|
1581
|
+
*
|
|
1582
|
+
* @param field Registered Protobuf field descriptor.
|
|
1583
|
+
* @returns The declared filter-value schema.
|
|
1584
|
+
*/ function filterValueSchema(field) {
|
|
1010
1585
|
if (field.fieldKind === "message")
|
|
1011
1586
|
return field.message;
|
|
1012
1587
|
if (field.fieldKind === "enum")
|
|
@@ -1036,18 +1611,29 @@ function filterValueSchema(field) {
|
|
|
1036
1611
|
return StringValueSchema;
|
|
1037
1612
|
return Int32ValueSchema;
|
|
1038
1613
|
}
|
|
1039
|
-
|
|
1614
|
+
/**
|
|
1615
|
+
* Checks special state columns against a wire filter value.
|
|
1616
|
+
*
|
|
1617
|
+
* @param column Registered Entity state column.
|
|
1618
|
+
* @param value Packed value for a system state column.
|
|
1619
|
+
* @returns Whether the special value matches.
|
|
1620
|
+
*/ function systemValueMatches(column, value) {
|
|
1040
1621
|
if (column === "version") {
|
|
1041
1622
|
return (value.typeUrl === TypeUrls.derive(VersionSchema) &&
|
|
1042
|
-
|
|
1623
|
+
AnyPacker.tryUnpack(value, VersionSchema) !== undefined);
|
|
1043
1624
|
}
|
|
1044
1625
|
if (column === "archived" || column === "deleted") {
|
|
1045
1626
|
return (value.typeUrl === TypeUrls.derive(BoolValueSchema) &&
|
|
1046
|
-
|
|
1627
|
+
AnyPacker.tryUnpack(value, BoolValueSchema) !== undefined);
|
|
1047
1628
|
}
|
|
1048
1629
|
return false;
|
|
1049
1630
|
}
|
|
1050
|
-
|
|
1631
|
+
/**
|
|
1632
|
+
* Checks whether a field type supports ordered comparisons.
|
|
1633
|
+
*
|
|
1634
|
+
* @param field Registered Protobuf field descriptor.
|
|
1635
|
+
* @returns Whether range comparisons are supported.
|
|
1636
|
+
*/ function supportsRange(field) {
|
|
1051
1637
|
if (field.fieldKind === "message") {
|
|
1052
1638
|
return (field.message.typeName === "google.protobuf.Timestamp" ||
|
|
1053
1639
|
field.message.typeName === "spine.core.Version");
|
|
@@ -1056,7 +1642,12 @@ function supportsRange(field) {
|
|
|
1056
1642
|
field.scalar !== ScalarType.BOOL &&
|
|
1057
1643
|
field.scalar !== ScalarType.BYTES);
|
|
1058
1644
|
}
|
|
1059
|
-
|
|
1645
|
+
/**
|
|
1646
|
+
* Maps a wire filter enum to the execution comparison.
|
|
1647
|
+
*
|
|
1648
|
+
* @param operator Wire filter comparison enum.
|
|
1649
|
+
* @returns The comparison operator.
|
|
1650
|
+
*/ function wireComparison(operator) {
|
|
1060
1651
|
if (operator === Filter_Operator.EQUAL)
|
|
1061
1652
|
return "equal";
|
|
1062
1653
|
if (operator === Filter_Operator.GREATER_THAN)
|
|
@@ -1069,25 +1660,43 @@ function wireComparison(operator) {
|
|
|
1069
1660
|
return "lessOrEqual";
|
|
1070
1661
|
return undefined;
|
|
1071
1662
|
}
|
|
1072
|
-
|
|
1663
|
+
/**
|
|
1664
|
+
* Creates a structured error for an unsupported filter.
|
|
1665
|
+
*
|
|
1666
|
+
* @param message Public validation or error message.
|
|
1667
|
+
* @returns The invalid-filter error.
|
|
1668
|
+
*/ function unsupportedFilterError(message) {
|
|
1073
1669
|
return invalidQueryError(message);
|
|
1074
1670
|
}
|
|
1075
|
-
|
|
1671
|
+
/**
|
|
1672
|
+
* Creates a structured error for an unsupported filter format.
|
|
1673
|
+
*
|
|
1674
|
+
* @param message Public validation or error message.
|
|
1675
|
+
* @returns The invalid-format error.
|
|
1676
|
+
*/ function unsupportedFormatError(message) {
|
|
1076
1677
|
return invalidQueryError(message);
|
|
1077
1678
|
}
|
|
1078
|
-
|
|
1679
|
+
/**
|
|
1680
|
+
* Creates the structured invalid-query error.
|
|
1681
|
+
*
|
|
1682
|
+
* @param message Public validation or error message.
|
|
1683
|
+
* @returns The invalid-query error.
|
|
1684
|
+
*/ function invalidQueryError(message) {
|
|
1079
1685
|
return {
|
|
1080
1686
|
type: "INVALID_QUERY",
|
|
1081
1687
|
message,
|
|
1082
1688
|
};
|
|
1083
1689
|
}
|
|
1084
|
-
|
|
1690
|
+
/**
|
|
1691
|
+
* Creates the structured invalid-subscription criterion error.
|
|
1692
|
+
*
|
|
1693
|
+
* @returns The invalid-criterion error.
|
|
1694
|
+
*/ function invalidCriterionError() {
|
|
1085
1695
|
return {
|
|
1086
1696
|
type: "INVALID_QUERY",
|
|
1087
1697
|
message: "QueryService.Read requires filters or include_all = true.",
|
|
1088
1698
|
};
|
|
1089
1699
|
}
|
|
1090
|
-
const DEFAULT_QUEUE_LIMIT = 100;
|
|
1091
1700
|
const DEFAULT_SUBSCRIPTION_LIMIT = 100;
|
|
1092
1701
|
/**
|
|
1093
1702
|
* Holds bounded request limits for QueryService.
|
|
@@ -1110,25 +1719,37 @@ const SubscriptionLimits = Object.freeze({
|
|
|
1110
1719
|
pathComponentCount: 16,
|
|
1111
1720
|
pathSegmentLength: 128,
|
|
1112
1721
|
});
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1722
|
+
/**
|
|
1723
|
+
* Validates the configured maximum concurrent subscriptions.
|
|
1724
|
+
*
|
|
1725
|
+
* @param value Configured maximum concurrent subscriptions.
|
|
1726
|
+
* @returns The accepted subscription limit.
|
|
1727
|
+
*/ function subscriptionLimit(value) {
|
|
1117
1728
|
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
1118
1729
|
throw new TypeError("SpineServices subscriptionLimit must be a positive safe integer.");
|
|
1119
1730
|
}
|
|
1120
1731
|
return value;
|
|
1121
1732
|
}
|
|
1122
|
-
|
|
1733
|
+
/**
|
|
1734
|
+
* Constructs an in-memory subscription record with its reader binding.
|
|
1735
|
+
*
|
|
1736
|
+
* @param input Fields for a new reader-bound subscription record.
|
|
1737
|
+
* @returns The new subscription record.
|
|
1738
|
+
*/ function createSubscriptionRecord(input) {
|
|
1123
1739
|
return {
|
|
1124
1740
|
id: input.id,
|
|
1125
1741
|
subscription: clone(SubscriptionSchema, input.subscription),
|
|
1742
|
+
...(input.reader === undefined ? {} : { reader: input.reader }),
|
|
1126
1743
|
tenantId: input.tenantId === undefined ? undefined : clone(TenantIdSchema, input.tenantId),
|
|
1127
|
-
delivery: new SubscriptionDelivery(input.
|
|
1744
|
+
delivery: new SubscriptionDelivery(input.queueCapacity),
|
|
1128
1745
|
...input.shape,
|
|
1129
1746
|
};
|
|
1130
1747
|
}
|
|
1131
|
-
|
|
1748
|
+
/**
|
|
1749
|
+
* Creates a shared removal task for concurrent cancellation.
|
|
1750
|
+
*
|
|
1751
|
+
* @returns The removal task and its completion promise.
|
|
1752
|
+
*/ function createSubscriptionRemoval() {
|
|
1132
1753
|
let reject;
|
|
1133
1754
|
let resolve;
|
|
1134
1755
|
const settled = new Promise((settle, fail) => {
|
|
@@ -1139,10 +1760,19 @@ function createSubscriptionRemoval() {
|
|
|
1139
1760
|
});
|
|
1140
1761
|
return { reject, resolve, settled };
|
|
1141
1762
|
}
|
|
1142
|
-
|
|
1763
|
+
/**
|
|
1764
|
+
* Deduplicates registered bounded contexts for routing.
|
|
1765
|
+
*
|
|
1766
|
+
* @param contexts Built bounded contexts available for routing.
|
|
1767
|
+
* @returns The distinct bounded contexts.
|
|
1768
|
+
*/ function uniqueContexts(contexts) {
|
|
1143
1769
|
return [...new Set(contexts)];
|
|
1144
1770
|
}
|
|
1145
|
-
|
|
1771
|
+
/**
|
|
1772
|
+
* Checks Topic identity, context, target and criterion.
|
|
1773
|
+
*
|
|
1774
|
+
* @param topic Framework subscription Topic.
|
|
1775
|
+
*/ function validateTopic(topic) {
|
|
1146
1776
|
if (topic.id?.value === undefined || topic.id.value.trim().length === 0) {
|
|
1147
1777
|
throw new ConnectError("Subscription topic ID is required.", Code.InvalidArgument);
|
|
1148
1778
|
}
|
|
@@ -1154,7 +1784,11 @@ function validateTopic(topic) {
|
|
|
1154
1784
|
}
|
|
1155
1785
|
}
|
|
1156
1786
|
/**
|
|
1157
|
-
* Validates a topic and
|
|
1787
|
+
* Validates a topic and returns its state or Event subscription shape.
|
|
1788
|
+
*
|
|
1789
|
+
* @param topic Framework subscription Topic.
|
|
1790
|
+
* @param route Registered target route with schema and tenancy.
|
|
1791
|
+
* @returns The normalized subscription shape.
|
|
1158
1792
|
*/
|
|
1159
1793
|
function createSubscriptionShape(topic, route) {
|
|
1160
1794
|
const target = topic.target;
|
|
@@ -1162,27 +1796,26 @@ function createSubscriptionShape(topic, route) {
|
|
|
1162
1796
|
throw new ConnectError("Subscription topic target is required.", Code.InvalidArgument);
|
|
1163
1797
|
}
|
|
1164
1798
|
if (route.kind === "event") {
|
|
1165
|
-
|
|
1166
|
-
return { kind: "event", route
|
|
1799
|
+
validateEventSubscriptionTopic(target);
|
|
1800
|
+
return { kind: "event", route };
|
|
1167
1801
|
}
|
|
1168
1802
|
switch (target.criterion.case) {
|
|
1169
1803
|
case "includeAll":
|
|
1170
1804
|
if (!target.criterion.value) {
|
|
1171
1805
|
throw new ConnectError("SubscriptionService.Subscribe requires filters or include_all = true.", Code.InvalidArgument);
|
|
1172
1806
|
}
|
|
1173
|
-
return { kind: "state", route
|
|
1807
|
+
return { kind: "state", route };
|
|
1174
1808
|
case "filters":
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
route,
|
|
1178
|
-
matcher: createFilteredSubscriptionMatcher(target.criterion.value, route),
|
|
1179
|
-
};
|
|
1809
|
+
ServiceValues.validateFilteredSubscriptionTopic(target.criterion.value, route);
|
|
1810
|
+
return { kind: "state", route };
|
|
1180
1811
|
default:
|
|
1181
1812
|
throw new ConnectError("SubscriptionService.Subscribe requires filters or include_all = true.", Code.InvalidArgument);
|
|
1182
1813
|
}
|
|
1183
1814
|
}
|
|
1184
1815
|
/**
|
|
1185
1816
|
* Checks the supported event topic criteria.
|
|
1817
|
+
*
|
|
1818
|
+
* @param target Declared query or Topic target.
|
|
1186
1819
|
*/
|
|
1187
1820
|
function validateEventSubscriptionTopic(target) {
|
|
1188
1821
|
switch (target.criterion.case) {
|
|
@@ -1190,45 +1823,19 @@ function validateEventSubscriptionTopic(target) {
|
|
|
1190
1823
|
if (!target.criterion.value) {
|
|
1191
1824
|
throw new ConnectError("SubscriptionService.Subscribe requires filters or include_all = true.", Code.InvalidArgument);
|
|
1192
1825
|
}
|
|
1193
|
-
return
|
|
1826
|
+
return;
|
|
1194
1827
|
case "filters":
|
|
1195
|
-
|
|
1828
|
+
EventOriginFilters.compile(target.criterion.value);
|
|
1829
|
+
return;
|
|
1196
1830
|
default:
|
|
1197
1831
|
throw new ConnectError("SubscriptionService.Subscribe requires filters or include_all = true.", Code.InvalidArgument);
|
|
1198
1832
|
}
|
|
1199
1833
|
}
|
|
1200
1834
|
/**
|
|
1201
|
-
*
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
const idValues = filters.idFilter?.id.map((id) => decodeSubscriptionIdValue(id, route));
|
|
1206
|
-
const statePredicate = createSubscriptionPredicate(filters.filter, route);
|
|
1207
|
-
return {
|
|
1208
|
-
/**
|
|
1209
|
-
* Matches one state update against the topic's ID and field filters.
|
|
1210
|
-
*
|
|
1211
|
-
* @param update State update to inspect.
|
|
1212
|
-
* @returns Delivery kind, if the update changes a matching state.
|
|
1213
|
-
*/
|
|
1214
|
-
match(update) {
|
|
1215
|
-
if (idValues !== undefined &&
|
|
1216
|
-
!idValues.some((id) => valuesEqual(update.id, id, route.idField.message))) {
|
|
1217
|
-
return undefined;
|
|
1218
|
-
}
|
|
1219
|
-
if (statePredicate(update.state))
|
|
1220
|
-
return "state";
|
|
1221
|
-
if (update.previousState !== undefined && statePredicate(update.previousState))
|
|
1222
|
-
return "noLongerMatching";
|
|
1223
|
-
return undefined;
|
|
1224
|
-
},
|
|
1225
|
-
};
|
|
1226
|
-
}
|
|
1227
|
-
function createSubscriptionPredicate(filters, route) {
|
|
1228
|
-
const predicates = filters.map((filter) => createCompositePredicate(filter, route));
|
|
1229
|
-
return (state) => predicates.every((predicate) => predicate(state));
|
|
1230
|
-
}
|
|
1231
|
-
function validateSubscriptionFilterTree(filters) {
|
|
1835
|
+
* Checks a Topic filter tree before accepting its definition.
|
|
1836
|
+
*
|
|
1837
|
+
* @param filters Target or subscription filter collection.
|
|
1838
|
+
*/ function validateSubscriptionFilterTree(filters) {
|
|
1232
1839
|
if (filters.length > SubscriptionLimits.compositeCount) {
|
|
1233
1840
|
throw new ConnectError("SubscriptionService.Subscribe may contain at most 8 composite filters.", Code.InvalidArgument);
|
|
1234
1841
|
}
|
|
@@ -1249,7 +1856,13 @@ function validateSubscriptionFilterTree(filters) {
|
|
|
1249
1856
|
enqueueSubscriptionFilterChildren(current.filter, current.depth, stack);
|
|
1250
1857
|
}
|
|
1251
1858
|
}
|
|
1252
|
-
|
|
1859
|
+
/**
|
|
1860
|
+
* Checks one nested subscription filter node and its bounds.
|
|
1861
|
+
*
|
|
1862
|
+
* @param filter One target or subscription filter.
|
|
1863
|
+
* @param depth Current composite-filter nesting depth.
|
|
1864
|
+
* @param counts Current filter traversal counts.
|
|
1865
|
+
*/ function validateSubscriptionFilterNode(filter, depth, counts) {
|
|
1253
1866
|
if (depth > SubscriptionLimits.compositeDepth) {
|
|
1254
1867
|
throw new ConnectError("SubscriptionService.Subscribe composite filters may nest at most 8 levels.", Code.InvalidArgument);
|
|
1255
1868
|
}
|
|
@@ -1276,7 +1889,13 @@ function validateSubscriptionFilterNode(filter, depth, counts) {
|
|
|
1276
1889
|
throw new ConnectError("SubscriptionService.Subscribe may contain at most 8 composite filters.", Code.InvalidArgument);
|
|
1277
1890
|
}
|
|
1278
1891
|
}
|
|
1279
|
-
|
|
1892
|
+
/**
|
|
1893
|
+
* Queues nested filter children for bounded traversal.
|
|
1894
|
+
*
|
|
1895
|
+
* @param filter One target or subscription filter.
|
|
1896
|
+
* @param depth Current composite-filter nesting depth.
|
|
1897
|
+
* @param stack Pending filter traversal stack.
|
|
1898
|
+
*/ function enqueueSubscriptionFilterChildren(filter, depth, stack) {
|
|
1280
1899
|
for (const nested of filter.compositeFilter) {
|
|
1281
1900
|
if (depth + 1 > SubscriptionLimits.compositeDepth) {
|
|
1282
1901
|
throw new ConnectError("SubscriptionService.Subscribe composite filters may nest at most 8 levels.", Code.InvalidArgument);
|
|
@@ -1284,32 +1903,11 @@ function enqueueSubscriptionFilterChildren(filter, depth, stack) {
|
|
|
1284
1903
|
stack.push({ filter: nested, depth: depth + 1 });
|
|
1285
1904
|
}
|
|
1286
1905
|
}
|
|
1287
|
-
function createCompositePredicate(filter, route) {
|
|
1288
|
-
const children = [
|
|
1289
|
-
...filter.filter.map((simple) => createSimplePredicate(simple, route)),
|
|
1290
|
-
...filter.compositeFilter.map((nested) => createCompositePredicate(nested, route)),
|
|
1291
|
-
];
|
|
1292
|
-
if (children.length === 0) {
|
|
1293
|
-
return () => true;
|
|
1294
|
-
}
|
|
1295
|
-
return filter.operator === CompositeFilter_CompositeOperator.ALL
|
|
1296
|
-
? (state) => children.every((predicate) => predicate(state))
|
|
1297
|
-
: (state) => children.some((predicate) => predicate(state));
|
|
1298
|
-
}
|
|
1299
|
-
function createSimplePredicate(filter, route) {
|
|
1300
|
-
if (filter.operator !== Filter_Operator.EQUAL) {
|
|
1301
|
-
throw new ConnectError("SubscriptionService.Subscribe supports only EQUAL field filters.", Code.InvalidArgument);
|
|
1302
|
-
}
|
|
1303
|
-
if (filter.value === undefined) {
|
|
1304
|
-
throw new ConnectError("SubscriptionService.Subscribe field filter value is required.", Code.InvalidArgument);
|
|
1305
|
-
}
|
|
1306
|
-
validateSubscriptionPath(filter.fieldPath?.fieldName ?? [], "field filter");
|
|
1307
|
-
const resolved = resolveMessagePath(route.schema, filter.fieldPath?.fieldName ?? [], "field filter");
|
|
1308
|
-
const expected = decodeFieldFilterValue(filter.value, resolved.leafSchema);
|
|
1309
|
-
return (state) => valuesEqual(readPathValue(state, resolved.localPath), expected, resolved.leafSchema);
|
|
1310
|
-
}
|
|
1311
1906
|
/**
|
|
1312
1907
|
* Checks a state filter path before resolving its fields.
|
|
1908
|
+
*
|
|
1909
|
+
* @param fieldPath Declared filter field path.
|
|
1910
|
+
* @param label Diagnostic label for the invalid input.
|
|
1313
1911
|
*/
|
|
1314
1912
|
function validateSubscriptionPath(fieldPath, label) {
|
|
1315
1913
|
if (fieldPath.length === 0 || fieldPath.some((field) => field.trim().length === 0)) {
|
|
@@ -1322,35 +1920,57 @@ function validateSubscriptionPath(fieldPath, label) {
|
|
|
1322
1920
|
throw new ConnectError(`SubscriptionService.Subscribe ${label} path components may contain at most 128 characters.`, Code.InvalidArgument);
|
|
1323
1921
|
}
|
|
1324
1922
|
}
|
|
1325
|
-
|
|
1923
|
+
/**
|
|
1924
|
+
* Decodes a subscription ID filter using the target ID schema.
|
|
1925
|
+
*
|
|
1926
|
+
* @param value Packed subscription ID Any.
|
|
1927
|
+
* @param route Registered target route with schema and tenancy.
|
|
1928
|
+
* @returns The typed subscription filter ID.
|
|
1929
|
+
*/ function decodeSubscriptionIdValue(value, route) {
|
|
1326
1930
|
if (!isAny(value)) {
|
|
1327
1931
|
throw new ConnectError("SubscriptionService.Subscribe id_filter values must be packed Any messages.", Code.InvalidArgument);
|
|
1328
1932
|
}
|
|
1329
1933
|
if (route.idField.message === undefined) {
|
|
1330
1934
|
return decodeAnyValue(value);
|
|
1331
1935
|
}
|
|
1332
|
-
const decoded =
|
|
1936
|
+
const decoded = route.idField.message.typeName === AnySchema.typeName
|
|
1937
|
+
? value
|
|
1938
|
+
: AnyPacker.tryUnpack(value, route.idField.message);
|
|
1333
1939
|
if (decoded === undefined) {
|
|
1334
1940
|
throw new ConnectError(`SubscriptionService.Subscribe id_filter values must pack ${route.idField.message.typeName}.`, Code.InvalidArgument);
|
|
1335
1941
|
}
|
|
1336
1942
|
return decoded;
|
|
1337
1943
|
}
|
|
1338
|
-
|
|
1944
|
+
/**
|
|
1945
|
+
* Decodes a field-filter Any with its declared value schema.
|
|
1946
|
+
*
|
|
1947
|
+
* @param value Packed field-filter Any, if present.
|
|
1948
|
+
* @param schema Registered Protobuf message schema.
|
|
1949
|
+
* @returns The typed field-filter value.
|
|
1950
|
+
*/ function decodeFieldFilterValue(value, schema) {
|
|
1339
1951
|
if (value === undefined || schema === undefined) {
|
|
1340
1952
|
return decodeAnyValue(value);
|
|
1341
1953
|
}
|
|
1342
|
-
const decoded =
|
|
1954
|
+
const decoded = schema.typeName === AnySchema.typeName ? value : AnyPacker.tryUnpack(value, schema);
|
|
1343
1955
|
if (decoded === undefined) {
|
|
1344
1956
|
throw new ConnectError(`SubscriptionService.Subscribe field filter value must pack ${schema.typeName}.`, Code.InvalidArgument);
|
|
1345
1957
|
}
|
|
1346
1958
|
return decoded;
|
|
1347
1959
|
}
|
|
1348
|
-
|
|
1960
|
+
/**
|
|
1961
|
+
* Decodes a known Any payload against its declared schema.
|
|
1962
|
+
*
|
|
1963
|
+
* @param value Packed Any payload, if present.
|
|
1964
|
+
* @param schema Registered Protobuf message schema.
|
|
1965
|
+
* @returns The decoded payload value.
|
|
1966
|
+
*/ function decodeAnyValue(value, schema) {
|
|
1349
1967
|
if (value === undefined) {
|
|
1350
1968
|
return undefined;
|
|
1351
1969
|
}
|
|
1352
1970
|
if (schema !== undefined) {
|
|
1353
|
-
|
|
1971
|
+
if (schema.typeName === AnySchema.typeName)
|
|
1972
|
+
return value;
|
|
1973
|
+
const decoded = AnyPacker.tryUnpack(value, schema);
|
|
1354
1974
|
if (decoded !== undefined) {
|
|
1355
1975
|
return decoded;
|
|
1356
1976
|
}
|
|
@@ -1365,9 +1985,13 @@ function decodeAnyValue(value, schema) {
|
|
|
1365
1985
|
}
|
|
1366
1986
|
/**
|
|
1367
1987
|
* Resolves a state filter path to generated message fields.
|
|
1988
|
+
*
|
|
1989
|
+
* @param schema Registered Protobuf message schema.
|
|
1990
|
+
* @param fieldPath Declared filter field path.
|
|
1991
|
+
* @param label Diagnostic label for the invalid input.
|
|
1992
|
+
* @returns The registered field at the declared path.
|
|
1368
1993
|
*/
|
|
1369
1994
|
function resolveMessagePath(schema, fieldPath, label) {
|
|
1370
|
-
const localPath = [];
|
|
1371
1995
|
let currentSchema = schema;
|
|
1372
1996
|
let leafSchema;
|
|
1373
1997
|
for (const [index, name] of fieldPath.entries()) {
|
|
@@ -1375,7 +1999,6 @@ function resolveMessagePath(schema, fieldPath, label) {
|
|
|
1375
1999
|
if (field === undefined) {
|
|
1376
2000
|
throw new ConnectError(`SubscriptionService.Subscribe ${label} "${fieldPath.join(".")}" is not a state field.`, Code.InvalidArgument);
|
|
1377
2001
|
}
|
|
1378
|
-
localPath.push(field.localName);
|
|
1379
2002
|
leafSchema = field.message;
|
|
1380
2003
|
currentSchema = index === fieldPath.length - 1 ? undefined : field.message;
|
|
1381
2004
|
if (currentSchema === undefined && index < fieldPath.length - 1) {
|
|
@@ -1383,45 +2006,35 @@ function resolveMessagePath(schema, fieldPath, label) {
|
|
|
1383
2006
|
}
|
|
1384
2007
|
}
|
|
1385
2008
|
return Object.freeze({
|
|
1386
|
-
localPath,
|
|
1387
2009
|
...(leafSchema === undefined ? {} : { leafSchema }),
|
|
1388
2010
|
});
|
|
1389
2011
|
}
|
|
1390
|
-
|
|
2012
|
+
/**
|
|
2013
|
+
* Finds a declared field in a registered Protobuf message.
|
|
2014
|
+
*
|
|
2015
|
+
* @param schema Registered Protobuf message schema.
|
|
2016
|
+
* @param name Configured header or field name.
|
|
2017
|
+
* @returns The matching field descriptor, if present.
|
|
2018
|
+
*/ function findMessageField(schema, name) {
|
|
1391
2019
|
const fields = (schema?.fields ?? []);
|
|
1392
2020
|
return fields.find((field) => field.name === name || field.localName === name);
|
|
1393
2021
|
}
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
current = Reflect.get(current, segment);
|
|
1401
|
-
}
|
|
1402
|
-
return current;
|
|
1403
|
-
}
|
|
1404
|
-
function valuesEqual(actual, expected, schema) {
|
|
1405
|
-
if (schema !== undefined && isMessage(actual) && isMessage(expected)) {
|
|
1406
|
-
return bytesEqual(toBinary(schema, actual), toBinary(schema, expected));
|
|
1407
|
-
}
|
|
1408
|
-
if (isAny(actual) && isAny(expected)) {
|
|
1409
|
-
return bytesEqual(toBinary(AnySchema, actual), toBinary(AnySchema, expected));
|
|
1410
|
-
}
|
|
1411
|
-
if (actual instanceof Uint8Array && expected instanceof Uint8Array) {
|
|
1412
|
-
return bytesEqual(actual, expected);
|
|
1413
|
-
}
|
|
1414
|
-
return Object.is(actual, expected);
|
|
1415
|
-
}
|
|
1416
|
-
function isMessage(value) {
|
|
2022
|
+
/**
|
|
2023
|
+
* Recognizes a Protobuf message value.
|
|
2024
|
+
*
|
|
2025
|
+
* @param value Candidate value to inspect as a Protobuf message.
|
|
2026
|
+
* @returns Whether the value is a message.
|
|
2027
|
+
*/ function isMessage(value) {
|
|
1417
2028
|
return typeof value === "object" && value !== null && "$typeName" in value;
|
|
1418
2029
|
}
|
|
1419
|
-
|
|
2030
|
+
/**
|
|
2031
|
+
* Recognizes a Protobuf Any message by its declared type name.
|
|
2032
|
+
*
|
|
2033
|
+
* @param value Candidate value to inspect as a Protobuf Any.
|
|
2034
|
+
* @returns Whether the value is an Any message.
|
|
2035
|
+
*/ function isAny(value) {
|
|
1420
2036
|
return isMessage(value) && value.$typeName === "google.protobuf.Any";
|
|
1421
2037
|
}
|
|
1422
|
-
function bytesEqual(left, right) {
|
|
1423
|
-
return left.length === right.length && left.every((byte, index) => byte === right[index]);
|
|
1424
|
-
}
|
|
1425
2038
|
/**
|
|
1426
2039
|
* Packs a state snapshot and version for a public query result.
|
|
1427
2040
|
*
|
|
@@ -1432,33 +2045,32 @@ function bytesEqual(left, right) {
|
|
|
1432
2045
|
*/
|
|
1433
2046
|
function packVersionedState(schema, result) {
|
|
1434
2047
|
return create(EntityStateVersionSchema, {
|
|
1435
|
-
state:
|
|
2048
|
+
state: AnyPacker.pack(schema, result.state),
|
|
1436
2049
|
version: result.version ?? create(VersionSchema),
|
|
1437
2050
|
});
|
|
1438
2051
|
}
|
|
1439
|
-
|
|
2052
|
+
/**
|
|
2053
|
+
* Extracts the declared tenant from a command actor context.
|
|
2054
|
+
*
|
|
2055
|
+
* @param command Framework command envelope.
|
|
2056
|
+
* @returns The declared command tenant, if present.
|
|
2057
|
+
*/ function commandTenant(command) {
|
|
1440
2058
|
return tenantValue(command.context?.actorContext?.tenantId);
|
|
1441
2059
|
}
|
|
1442
|
-
|
|
2060
|
+
/**
|
|
2061
|
+
* Extracts the declared tenant from a Topic actor context.
|
|
2062
|
+
*
|
|
2063
|
+
* @param topic Framework subscription Topic.
|
|
2064
|
+
* @returns The Topic tenant, if present.
|
|
2065
|
+
*/ function topicTenant(topic) {
|
|
1443
2066
|
return tenantValue(topic?.context?.tenantId);
|
|
1444
2067
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
default:
|
|
1452
|
-
return undefined;
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
function eventTenantMatches(record, event) {
|
|
1456
|
-
const tenantId = eventTenant(event);
|
|
1457
|
-
return (record.tenantId === undefined ||
|
|
1458
|
-
(tenantId !== undefined &&
|
|
1459
|
-
TenantBoundary.from(tenantId).key === TenantBoundary.from(record.tenantId).key));
|
|
1460
|
-
}
|
|
1461
|
-
function tenantValue(tenantId) {
|
|
2068
|
+
/**
|
|
2069
|
+
* Extracts the tenant value from a Proto tenant identifier.
|
|
2070
|
+
*
|
|
2071
|
+
* @param tenantId Declared Proto tenant identifier.
|
|
2072
|
+
* @returns The tenant value, if present.
|
|
2073
|
+
*/ function tenantValue(tenantId) {
|
|
1462
2074
|
if (tenantId === undefined)
|
|
1463
2075
|
return undefined;
|
|
1464
2076
|
try {
|
|
@@ -1468,7 +2080,13 @@ function tenantValue(tenantId) {
|
|
|
1468
2080
|
return undefined;
|
|
1469
2081
|
}
|
|
1470
2082
|
}
|
|
1471
|
-
|
|
2083
|
+
/**
|
|
2084
|
+
* Finds the declared Entity ID field for a registered state route.
|
|
2085
|
+
*
|
|
2086
|
+
* @param schema Registered Protobuf message schema.
|
|
2087
|
+
* @param idField Registered Entity ID field.
|
|
2088
|
+
* @returns The Entity ID field descriptor.
|
|
2089
|
+
*/ function stateRouteIdField(schema, idField) {
|
|
1472
2090
|
const field = findMessageField(schema, idField?.name ?? "") ??
|
|
1473
2091
|
findMessageField(schema, idField?.localName ?? "") ??
|
|
1474
2092
|
schema.fields[0] ??
|
|
@@ -1478,7 +2096,52 @@ function stateRouteIdField(schema, idField) {
|
|
|
1478
2096
|
}
|
|
1479
2097
|
return field;
|
|
1480
2098
|
}
|
|
1481
|
-
|
|
2099
|
+
/**
|
|
2100
|
+
* Maps confirmed tenant absence to native service errors.
|
|
2101
|
+
*/
|
|
2102
|
+
const TenantStatuses = Object.freeze({
|
|
2103
|
+
/**
|
|
2104
|
+
* Checks live membership and maps only confirmed missing membership to NotFound.
|
|
2105
|
+
*
|
|
2106
|
+
* @param context Receiving context and its selected index.
|
|
2107
|
+
* @param tenantId Complete selected tenant identity.
|
|
2108
|
+
* @param onReady Complete one-shot mutation after membership and preparation.
|
|
2109
|
+
* @returns Completion after membership and any mutation settle.
|
|
2110
|
+
*/
|
|
2111
|
+
async require(context, tenantId, onReady) {
|
|
2112
|
+
try {
|
|
2113
|
+
const runtimeContext = context;
|
|
2114
|
+
if (runtimeContext instanceof BoundedContext)
|
|
2115
|
+
await boundedContextAccess.admitServiceInput(runtimeContext, tenantId, () => TenantAdmissions.require(context.tenantIndex, context.name.value, context.isMultitenant, tenantId), onReady);
|
|
2116
|
+
else {
|
|
2117
|
+
await TenantAdmissions.require(context.tenantIndex, context.name.value, context.isMultitenant, tenantId);
|
|
2118
|
+
await onReady?.();
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
catch (error) {
|
|
2122
|
+
if (error instanceof TenantUnknownError)
|
|
2123
|
+
throw TenantStatuses.unknown(error);
|
|
2124
|
+
throw error;
|
|
2125
|
+
}
|
|
2126
|
+
},
|
|
2127
|
+
/**
|
|
2128
|
+
* Exposes a confirmed unknown tenant as a native NotFound RPC with Spine details.
|
|
2129
|
+
*
|
|
2130
|
+
* @param error Receiving-context membership failure.
|
|
2131
|
+
* @returns Connect failure preserving its stable Spine error type.
|
|
2132
|
+
*/
|
|
2133
|
+
unknown(error) {
|
|
2134
|
+
return ReadRequestStatus.unknownTenant(error);
|
|
2135
|
+
},
|
|
2136
|
+
});
|
|
2137
|
+
/**
|
|
2138
|
+
* Checks a request tenant against bounded-context tenancy.
|
|
2139
|
+
*
|
|
2140
|
+
* @param multitenant Whether the bounded context requires a tenant.
|
|
2141
|
+
* @param tenantId Declared Proto tenant identifier.
|
|
2142
|
+
* @param subject Verified request subject.
|
|
2143
|
+
* @returns The tenant mismatch status, if any.
|
|
2144
|
+
*/ function tenantMismatch(multitenant, tenantId, subject) {
|
|
1482
2145
|
if (multitenant) {
|
|
1483
2146
|
return tenantId === undefined
|
|
1484
2147
|
? {
|
|
@@ -1494,23 +2157,25 @@ function tenantMismatch(multitenant, tenantId, subject) {
|
|
|
1494
2157
|
message: `Tenant is not applicable for this ${subject}.`,
|
|
1495
2158
|
};
|
|
1496
2159
|
}
|
|
1497
|
-
|
|
2160
|
+
/**
|
|
2161
|
+
* Maps a command-post failure to its public structured status.
|
|
2162
|
+
*
|
|
2163
|
+
* @param error The failure being classified or propagated.
|
|
2164
|
+
* @returns The sanitized command status.
|
|
2165
|
+
*/ function commandPostError(error) {
|
|
1498
2166
|
if (error instanceof TransitionValidationError) {
|
|
1499
2167
|
return {
|
|
1500
2168
|
type: error.type,
|
|
1501
2169
|
message: error.clientMessage,
|
|
1502
|
-
details:
|
|
1503
|
-
validate: false,
|
|
1504
|
-
}),
|
|
2170
|
+
details: AnyPacker.pack(ValidationErrorSchema, error.validationError),
|
|
1505
2171
|
};
|
|
1506
2172
|
}
|
|
1507
2173
|
if (error instanceof CommandValidationError) {
|
|
1508
2174
|
return {
|
|
1509
|
-
type: "
|
|
2175
|
+
type: "spine.core.CommandValidationError",
|
|
2176
|
+
code: CommandValidationErrorCode.INVALID_COMMAND,
|
|
1510
2177
|
message: "Command payload validation failed.",
|
|
1511
|
-
details:
|
|
1512
|
-
validate: false,
|
|
1513
|
-
}),
|
|
2178
|
+
details: AnyPacker.pack(ValidationErrorSchema, error.validationError),
|
|
1514
2179
|
};
|
|
1515
2180
|
}
|
|
1516
2181
|
return {
|
|
@@ -1578,108 +2243,39 @@ const InternalErrorFacts = Object.freeze({
|
|
|
1578
2243
|
}
|
|
1579
2244
|
},
|
|
1580
2245
|
});
|
|
1581
|
-
function tenantOptions(tenantId) {
|
|
1582
|
-
return tenantId === undefined ? {} : { tenantId: clone(TenantIdSchema, tenantId) };
|
|
1583
|
-
}
|
|
1584
2246
|
const VALUE_DECODERS = Object.freeze([
|
|
1585
|
-
(value) =>
|
|
1586
|
-
(value) =>
|
|
1587
|
-
(value) =>
|
|
1588
|
-
(value) =>
|
|
1589
|
-
(value) =>
|
|
1590
|
-
(value) =>
|
|
1591
|
-
(value) =>
|
|
1592
|
-
(value) =>
|
|
1593
|
-
(value) =>
|
|
2247
|
+
(value) => AnyPacker.tryUnpack(value, StringValueSchema)?.value,
|
|
2248
|
+
(value) => AnyPacker.tryUnpack(value, BoolValueSchema)?.value,
|
|
2249
|
+
(value) => AnyPacker.tryUnpack(value, Int32ValueSchema)?.value,
|
|
2250
|
+
(value) => AnyPacker.tryUnpack(value, UInt32ValueSchema)?.value,
|
|
2251
|
+
(value) => AnyPacker.tryUnpack(value, Int64ValueSchema)?.value,
|
|
2252
|
+
(value) => AnyPacker.tryUnpack(value, UInt64ValueSchema)?.value,
|
|
2253
|
+
(value) => AnyPacker.tryUnpack(value, FloatValueSchema)?.value,
|
|
2254
|
+
(value) => AnyPacker.tryUnpack(value, DoubleValueSchema)?.value,
|
|
2255
|
+
(value) => AnyPacker.tryUnpack(value, BytesValueSchema)?.value,
|
|
1594
2256
|
]);
|
|
1595
|
-
/**
|
|
1596
|
-
* Builds a subscription update with a complete state or a removal signal.
|
|
1597
|
-
*/
|
|
1598
|
-
function createEntityUpdate(record, update) {
|
|
1599
|
-
const matcher = record.matcher;
|
|
1600
|
-
if (matcher === undefined)
|
|
1601
|
-
return undefined;
|
|
1602
|
-
const match = matcher.match(update);
|
|
1603
|
-
if (match === undefined)
|
|
1604
|
-
return undefined;
|
|
1605
|
-
const kind = match === "state"
|
|
1606
|
-
? {
|
|
1607
|
-
case: "state",
|
|
1608
|
-
value: AnyMessages.pack(record.route.schema, clone(record.route.schema, update.state), {
|
|
1609
|
-
validate: false,
|
|
1610
|
-
}),
|
|
1611
|
-
}
|
|
1612
|
-
: { case: "noLongerMatching", value: true };
|
|
1613
|
-
return create(SubscriptionUpdateSchema, {
|
|
1614
|
-
subscription: clone(SubscriptionSchema, record.subscription),
|
|
1615
|
-
response: okResponse(),
|
|
1616
|
-
update: {
|
|
1617
|
-
case: "entityUpdates",
|
|
1618
|
-
value: create(EntityUpdatesSchema, {
|
|
1619
|
-
update: [
|
|
1620
|
-
create(EntityStateUpdateSchema, {
|
|
1621
|
-
id: packEntityId(record.route, update.id),
|
|
1622
|
-
kind,
|
|
1623
|
-
}),
|
|
1624
|
-
],
|
|
1625
|
-
}),
|
|
1626
|
-
},
|
|
1627
|
-
});
|
|
1628
|
-
}
|
|
1629
|
-
function createEventUpdate(record, event) {
|
|
1630
|
-
return create(SubscriptionUpdateSchema, {
|
|
1631
|
-
subscription: clone(SubscriptionSchema, record.subscription),
|
|
1632
|
-
response: okResponse(),
|
|
1633
|
-
update: {
|
|
1634
|
-
case: "eventUpdates",
|
|
1635
|
-
value: create(EventUpdatesSchema, {
|
|
1636
|
-
event: [cloneClientEvent(event)],
|
|
1637
|
-
}),
|
|
1638
|
-
},
|
|
1639
|
-
});
|
|
1640
|
-
}
|
|
1641
|
-
function cloneClientEvent(event) {
|
|
1642
|
-
const clientEvent = clone(EventSchema, event);
|
|
1643
|
-
const rejection = clientEvent.context?.rejection;
|
|
1644
|
-
if (rejection !== undefined) {
|
|
1645
|
-
rejection.command = undefined;
|
|
1646
|
-
// Clear the legacy wire payload as part of client-side security redaction.
|
|
1647
|
-
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
1648
|
-
rejection.commandMessage = undefined;
|
|
1649
|
-
rejection.stacktrace = "";
|
|
1650
|
-
}
|
|
1651
|
-
return clientEvent;
|
|
1652
|
-
}
|
|
1653
|
-
function packString(value) {
|
|
1654
|
-
return AnyMessages.pack(StringValueSchema, create(StringValueSchema, { value }));
|
|
1655
|
-
}
|
|
1656
|
-
function packEntityId(route, id) {
|
|
1657
|
-
if (isAny(id)) {
|
|
1658
|
-
return clone(AnySchema, id);
|
|
1659
|
-
}
|
|
1660
|
-
if (route.idField.message !== undefined && isMessage(id)) {
|
|
1661
|
-
return AnyMessages.pack(route.idField.message, id, { validate: false });
|
|
1662
|
-
}
|
|
1663
|
-
if (id instanceof Uint8Array) {
|
|
1664
|
-
return AnyMessages.pack(BytesValueSchema, create(BytesValueSchema, { value: id }));
|
|
1665
|
-
}
|
|
1666
|
-
switch (typeof id) {
|
|
1667
|
-
case "string":
|
|
1668
|
-
return packString(id);
|
|
1669
|
-
case "boolean":
|
|
1670
|
-
return AnyMessages.pack(BoolValueSchema, create(BoolValueSchema, { value: id }));
|
|
1671
|
-
case "number":
|
|
1672
|
-
return AnyMessages.pack(DoubleValueSchema, create(DoubleValueSchema, { value: id }));
|
|
1673
|
-
case "bigint":
|
|
1674
|
-
return AnyMessages.pack(Int64ValueSchema, create(Int64ValueSchema, { value: id }));
|
|
1675
|
-
default:
|
|
1676
|
-
return undefined;
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
2257
|
/**
|
|
1680
2258
|
* Groups non-public service contract, query, and subscription operations.
|
|
1681
2259
|
*/
|
|
1682
2260
|
const ServiceValues = Object.freeze({
|
|
2261
|
+
/**
|
|
2262
|
+
* Maps supported Topic shape errors to structured public failures.
|
|
2263
|
+
*
|
|
2264
|
+
* @param topic Constraint-valid Topic to inspect.
|
|
2265
|
+
* @param route Registered state or Event route.
|
|
2266
|
+
*/
|
|
2267
|
+
checkTopicSemantics(topic, route) {
|
|
2268
|
+
try {
|
|
2269
|
+
ServiceValues.validateTopic(topic);
|
|
2270
|
+
ServiceValues.createSubscriptionShape(topic, route);
|
|
2271
|
+
}
|
|
2272
|
+
catch (error) {
|
|
2273
|
+
if (error instanceof ConnectError && error.code === Code.InvalidArgument) {
|
|
2274
|
+
throw ReadRequestStatus.invalid("topic", false, error.rawMessage);
|
|
2275
|
+
}
|
|
2276
|
+
throw new ConnectError("Topic validation failed.", Code.Internal);
|
|
2277
|
+
}
|
|
2278
|
+
},
|
|
1683
2279
|
/**
|
|
1684
2280
|
* Checks one composite query filter and its current traversal bounds.
|
|
1685
2281
|
*
|
|
@@ -1733,6 +2329,48 @@ const ServiceValues = Object.freeze({
|
|
|
1733
2329
|
}
|
|
1734
2330
|
return undefined;
|
|
1735
2331
|
},
|
|
2332
|
+
/**
|
|
2333
|
+
* Validates state ID and field filters before storing the topic.
|
|
2334
|
+
*
|
|
2335
|
+
* @param filters The topic's declared filters.
|
|
2336
|
+
* @param route The registered state route.
|
|
2337
|
+
*/
|
|
2338
|
+
validateFilteredSubscriptionTopic(filters, route) {
|
|
2339
|
+
ServiceValues.validateSubscriptionFilters(filters);
|
|
2340
|
+
for (const id of filters.idFilter?.id ?? [])
|
|
2341
|
+
decodeSubscriptionIdValue(id, route);
|
|
2342
|
+
for (const filter of filters.filter)
|
|
2343
|
+
ServiceValues.validateCompositeFilter(filter, route);
|
|
2344
|
+
},
|
|
2345
|
+
/**
|
|
2346
|
+
* Validates nested state filter values without retaining an observer predicate.
|
|
2347
|
+
*
|
|
2348
|
+
* @param filter A validated composite filter.
|
|
2349
|
+
* @param route The registered state route.
|
|
2350
|
+
*/
|
|
2351
|
+
validateCompositeFilter(filter, route) {
|
|
2352
|
+
for (const simple of filter.filter)
|
|
2353
|
+
ServiceValues.validateSimpleFilter(simple, route);
|
|
2354
|
+
for (const nested of filter.compositeFilter)
|
|
2355
|
+
ServiceValues.validateCompositeFilter(nested, route);
|
|
2356
|
+
},
|
|
2357
|
+
/**
|
|
2358
|
+
* Validates one field path and packed comparison value.
|
|
2359
|
+
*
|
|
2360
|
+
* @param filter A simple field filter.
|
|
2361
|
+
* @param route The registered state route.
|
|
2362
|
+
*/
|
|
2363
|
+
validateSimpleFilter(filter, route) {
|
|
2364
|
+
if (filter.operator !== Filter_Operator.EQUAL) {
|
|
2365
|
+
throw new ConnectError("SubscriptionService.Subscribe supports only EQUAL field filters.", Code.InvalidArgument);
|
|
2366
|
+
}
|
|
2367
|
+
if (filter.value === undefined) {
|
|
2368
|
+
throw new ConnectError("SubscriptionService.Subscribe field filter value is required.", Code.InvalidArgument);
|
|
2369
|
+
}
|
|
2370
|
+
validateSubscriptionPath(filter.fieldPath?.fieldName ?? [], "field filter");
|
|
2371
|
+
const resolved = resolveMessagePath(route.schema, filter.fieldPath?.fieldName ?? [], "field filter");
|
|
2372
|
+
decodeFieldFilterValue(filter.value, resolved.leafSchema);
|
|
2373
|
+
},
|
|
1736
2374
|
/**
|
|
1737
2375
|
* Checks that a state topic has a usable ID or field filter.
|
|
1738
2376
|
*
|
|
@@ -1755,34 +2393,29 @@ const ServiceValues = Object.freeze({
|
|
|
1755
2393
|
* Builds a stable Command rejection acknowledgement.
|
|
1756
2394
|
*
|
|
1757
2395
|
* @param messageId The packed Command ID, if supplied.
|
|
1758
|
-
* @param type The
|
|
2396
|
+
* @param type The error type.
|
|
1759
2397
|
* @param message The public error message.
|
|
2398
|
+
* @param code Numeric Proto error code when available.
|
|
2399
|
+
* @param details Packed structured validation details when available.
|
|
1760
2400
|
* @returns A sanitized Ack.
|
|
1761
2401
|
*/
|
|
1762
|
-
commandErrorAck(messageId, type, message) {
|
|
1763
|
-
return create(AckSchema, { messageId, status: errorStatus(type, message) });
|
|
2402
|
+
commandErrorAck(messageId, type, message, code, details) {
|
|
2403
|
+
return create(AckSchema, { messageId, status: errorStatus(type, message, details, code) });
|
|
1764
2404
|
},
|
|
1765
2405
|
commandTenant,
|
|
1766
|
-
createEntityUpdate,
|
|
1767
|
-
createEventUpdate,
|
|
1768
2406
|
createReadPlan,
|
|
1769
2407
|
createSubscriptionRecord,
|
|
1770
2408
|
createSubscriptionRemoval,
|
|
1771
2409
|
createSubscriptionShape,
|
|
1772
|
-
defaultQueueLimit: DEFAULT_QUEUE_LIMIT,
|
|
1773
2410
|
defaultSubscriptionLimit: DEFAULT_SUBSCRIPTION_LIMIT,
|
|
1774
2411
|
errorStatus,
|
|
1775
|
-
eventTenantMatches,
|
|
1776
2412
|
okResponse,
|
|
1777
2413
|
okStatus,
|
|
1778
|
-
positiveInteger,
|
|
1779
2414
|
packVersionedState,
|
|
1780
|
-
queryErrorResponse,
|
|
1781
2415
|
queryResultLimit: QueryLimits.resultCount,
|
|
1782
2416
|
stateRouteIdField,
|
|
1783
2417
|
subscriptionLimit,
|
|
1784
2418
|
tenantMismatch,
|
|
1785
|
-
tenantOptions,
|
|
1786
2419
|
tenantValue,
|
|
1787
2420
|
topicTenant,
|
|
1788
2421
|
uniqueContexts,
|