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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/README.md +225 -518
  2. package/REFERENCE.md +498 -49
  3. package/dist/browser/browser-server.d.ts +95 -0
  4. package/dist/browser/browser-server.d.ts.map +1 -0
  5. package/dist/{server → browser}/browser-server.js +93 -7
  6. package/dist/browser/browser-server.js.map +1 -0
  7. package/dist/browser/durable-subscription-bindings.d.ts.map +1 -0
  8. package/dist/browser/durable-subscription-bindings.js.map +1 -0
  9. package/dist/browser/index.d.ts +12 -0
  10. package/dist/browser/index.d.ts.map +1 -0
  11. package/dist/browser/index.js +24 -0
  12. package/dist/browser/index.js.map +1 -0
  13. package/dist/browser/options.d.ts +183 -0
  14. package/dist/browser/options.d.ts.map +1 -0
  15. package/dist/browser/options.js +15 -0
  16. package/dist/browser/options.js.map +1 -0
  17. package/dist/bus/command-bus.d.ts +2 -0
  18. package/dist/bus/command-bus.d.ts.map +1 -1
  19. package/dist/bus/command-bus.js +36 -1
  20. package/dist/bus/command-bus.js.map +1 -1
  21. package/dist/bus/event-dispatcher-registry.d.ts +2 -2
  22. package/dist/bus/event-dispatcher-registry.js +3 -3
  23. package/dist/bus/event-dispatcher-registry.js.map +1 -1
  24. package/dist/context/bounded-context.d.ts +238 -83
  25. package/dist/context/bounded-context.d.ts.map +1 -1
  26. package/dist/context/bounded-context.js +1083 -286
  27. package/dist/context/bounded-context.js.map +1 -1
  28. package/dist/context/effective-tenant.d.ts +9 -0
  29. package/dist/context/effective-tenant.d.ts.map +1 -0
  30. package/dist/context/effective-tenant.js +58 -0
  31. package/dist/context/effective-tenant.js.map +1 -0
  32. package/dist/context/entity-inbox.d.ts.map +1 -1
  33. package/dist/context/entity-inbox.js +24 -17
  34. package/dist/context/entity-inbox.js.map +1 -1
  35. package/dist/context/local-inbox-handoff.d.ts +7 -3
  36. package/dist/context/local-inbox-handoff.d.ts.map +1 -1
  37. package/dist/context/local-inbox-handoff.js +32 -26
  38. package/dist/context/local-inbox-handoff.js.map +1 -1
  39. package/dist/context/projection-handoff.d.ts.map +1 -1
  40. package/dist/context/projection-handoff.js +36 -31
  41. package/dist/context/projection-handoff.js.map +1 -1
  42. package/dist/context/tenant-index.d.ts.map +1 -1
  43. package/dist/context/tenant-index.js +72 -2
  44. package/dist/context/tenant-index.js.map +1 -1
  45. package/dist/delivery/delivery-ports.d.ts +14 -4
  46. package/dist/delivery/delivery-ports.d.ts.map +1 -1
  47. package/dist/delivery/delivery.d.ts +19 -21
  48. package/dist/delivery/delivery.d.ts.map +1 -1
  49. package/dist/delivery/delivery.js +541 -158
  50. package/dist/delivery/delivery.js.map +1 -1
  51. package/dist/delivery/inbox-storage.d.ts +12 -3
  52. package/dist/delivery/inbox-storage.d.ts.map +1 -1
  53. package/dist/delivery/inbox-storage.js +160 -67
  54. package/dist/delivery/inbox-storage.js.map +1 -1
  55. package/dist/delivery/inbox.d.ts +9 -0
  56. package/dist/delivery/inbox.d.ts.map +1 -1
  57. package/dist/delivery/inbox.js +11 -0
  58. package/dist/delivery/inbox.js.map +1 -1
  59. package/dist/delivery/sharded-work-registry.d.ts.map +1 -1
  60. package/dist/delivery/sharded-work-registry.js +2 -1
  61. package/dist/delivery/sharded-work-registry.js.map +1 -1
  62. package/dist/entity/entity-metadata.d.ts +14 -5
  63. package/dist/entity/entity-metadata.d.ts.map +1 -1
  64. package/dist/entity/entity-metadata.js +38 -2
  65. package/dist/entity/entity-metadata.js.map +1 -1
  66. package/dist/entity/entity-storage-descriptor.d.ts +25 -5
  67. package/dist/entity/entity-storage-descriptor.d.ts.map +1 -1
  68. package/dist/entity/entity-storage-descriptor.js +69 -23
  69. package/dist/entity/entity-storage-descriptor.js.map +1 -1
  70. package/dist/entity/entity-transaction.d.ts +46 -42
  71. package/dist/entity/entity-transaction.d.ts.map +1 -1
  72. package/dist/entity/entity-transaction.js +130 -35
  73. package/dist/entity/entity-transaction.js.map +1 -1
  74. package/dist/entity/entity.d.ts +243 -98
  75. package/dist/entity/entity.d.ts.map +1 -1
  76. package/dist/entity/entity.js +405 -226
  77. package/dist/entity/entity.js.map +1 -1
  78. package/dist/entity/spec-scanner.d.ts +13 -1
  79. package/dist/entity/spec-scanner.d.ts.map +1 -1
  80. package/dist/entity/spec-scanner.js +20 -3
  81. package/dist/entity/spec-scanner.js.map +1 -1
  82. package/dist/handler/command-registration-readiness.d.ts +6 -6
  83. package/dist/handler/command-registration-readiness.d.ts.map +1 -1
  84. package/dist/handler/command-registration-readiness.js +4 -2
  85. package/dist/handler/command-registration-readiness.js.map +1 -1
  86. package/dist/handler/declared-rejections.d.ts +11 -0
  87. package/dist/handler/declared-rejections.d.ts.map +1 -0
  88. package/dist/handler/declared-rejections.js +34 -0
  89. package/dist/handler/declared-rejections.js.map +1 -0
  90. package/dist/handler/event-registration-readiness.d.ts +13 -56
  91. package/dist/handler/event-registration-readiness.d.ts.map +1 -1
  92. package/dist/handler/event-registration-readiness.js +23 -43
  93. package/dist/handler/event-registration-readiness.js.map +1 -1
  94. package/dist/handler/generated-handler-registry.d.ts +72 -33
  95. package/dist/handler/generated-handler-registry.d.ts.map +1 -1
  96. package/dist/handler/generated-handler-registry.js +421 -43
  97. package/dist/handler/generated-handler-registry.js.map +1 -1
  98. package/dist/handler/generated-registry-discovery.d.ts.map +1 -1
  99. package/dist/handler/generated-registry-discovery.js +2 -4
  100. package/dist/handler/generated-registry-discovery.js.map +1 -1
  101. package/dist/handler/handler-decorators.d.ts +91 -37
  102. package/dist/handler/handler-decorators.d.ts.map +1 -1
  103. package/dist/handler/handler-decorators.js +124 -31
  104. package/dist/handler/handler-decorators.js.map +1 -1
  105. package/dist/handler/handler-metadata.d.ts +105 -60
  106. package/dist/handler/handler-metadata.d.ts.map +1 -1
  107. package/dist/handler/handler-metadata.js +275 -57
  108. package/dist/handler/handler-metadata.js.map +1 -1
  109. package/dist/handler/registration-readiness-metadata.d.ts +4 -0
  110. package/dist/handler/registration-readiness-metadata.d.ts.map +1 -1
  111. package/dist/handler/registration-readiness-metadata.js +84 -2
  112. package/dist/handler/registration-readiness-metadata.js.map +1 -1
  113. package/dist/handler/standalone.d.ts +25 -0
  114. package/dist/handler/standalone.d.ts.map +1 -0
  115. package/dist/handler/standalone.js +38 -0
  116. package/dist/handler/standalone.js.map +1 -0
  117. package/dist/index.d.ts +9 -9
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +3 -3
  120. package/dist/index.js.map +1 -1
  121. package/dist/repository/command-routing.d.ts +63 -10
  122. package/dist/repository/command-routing.d.ts.map +1 -1
  123. package/dist/repository/command-routing.js +41 -8
  124. package/dist/repository/command-routing.js.map +1 -1
  125. package/dist/repository/event-routing.d.ts +64 -9
  126. package/dist/repository/event-routing.d.ts.map +1 -1
  127. package/dist/repository/event-routing.js +42 -7
  128. package/dist/repository/event-routing.js.map +1 -1
  129. package/dist/repository/primitive-id.d.ts +12 -23
  130. package/dist/repository/primitive-id.d.ts.map +1 -1
  131. package/dist/repository/primitive-id.js +2 -27
  132. package/dist/repository/primitive-id.js.map +1 -1
  133. package/dist/repository/repository.d.ts +317 -87
  134. package/dist/repository/repository.d.ts.map +1 -1
  135. package/dist/repository/repository.js +3746 -856
  136. package/dist/repository/repository.js.map +1 -1
  137. package/dist/repository/state-update-routing.d.ts +67 -8
  138. package/dist/repository/state-update-routing.d.ts.map +1 -1
  139. package/dist/repository/state-update-routing.js +45 -6
  140. package/dist/repository/state-update-routing.js.map +1 -1
  141. package/dist/runtime/runtime.js +1 -1
  142. package/dist/runtime/runtime.js.map +1 -1
  143. package/dist/runtime/signal-metadata.d.ts +22 -47
  144. package/dist/runtime/signal-metadata.d.ts.map +1 -1
  145. package/dist/runtime/signal-metadata.js +34 -69
  146. package/dist/runtime/signal-metadata.js.map +1 -1
  147. package/dist/runtime/signal-publisher.d.ts +144 -0
  148. package/dist/runtime/signal-publisher.d.ts.map +1 -0
  149. package/dist/runtime/signal-publisher.js +248 -0
  150. package/dist/runtime/signal-publisher.js.map +1 -0
  151. package/dist/runtime/standalone-handler-runtime.d.ts +130 -0
  152. package/dist/runtime/standalone-handler-runtime.d.ts.map +1 -0
  153. package/dist/runtime/standalone-handler-runtime.js +382 -0
  154. package/dist/runtime/standalone-handler-runtime.js.map +1 -0
  155. package/dist/server/environment-attachment.d.ts.map +1 -1
  156. package/dist/server/environment-attachment.js +2 -1
  157. package/dist/server/environment-attachment.js.map +1 -1
  158. package/dist/server/environment-delivery-worker.d.ts.map +1 -1
  159. package/dist/server/environment-delivery-worker.js +21 -12
  160. package/dist/server/environment-delivery-worker.js.map +1 -1
  161. package/dist/server/managed-server-application.d.ts +4 -1
  162. package/dist/server/managed-server-application.d.ts.map +1 -1
  163. package/dist/server/managed-server-application.js +2 -2
  164. package/dist/server/managed-server-application.js.map +1 -1
  165. package/dist/server/node-coordinator.js +2 -2
  166. package/dist/server/node-coordinator.js.map +1 -1
  167. package/dist/server/server-log.d.ts.map +1 -1
  168. package/dist/server/server-log.js +4 -0
  169. package/dist/server/server-log.js.map +1 -1
  170. package/dist/server/server.d.ts +9 -177
  171. package/dist/server/server.d.ts.map +1 -1
  172. package/dist/server/server.js +428 -178
  173. package/dist/server/server.js.map +1 -1
  174. package/dist/services/query-reader.d.ts +19 -0
  175. package/dist/services/query-reader.d.ts.map +1 -0
  176. package/dist/services/query-reader.js +43 -0
  177. package/dist/services/query-reader.js.map +1 -0
  178. package/dist/services/registered-targets.d.ts +73 -0
  179. package/dist/services/registered-targets.d.ts.map +1 -0
  180. package/dist/services/registered-targets.js +127 -0
  181. package/dist/services/registered-targets.js.map +1 -0
  182. package/dist/services/spine-services.d.ts +26 -12
  183. package/dist/services/spine-services.d.ts.map +1 -1
  184. package/dist/services/spine-services.js +1047 -969
  185. package/dist/services/spine-services.js.map +1 -1
  186. package/dist/spi/delivery.d.ts +5 -0
  187. package/dist/spi/delivery.d.ts.map +1 -0
  188. package/dist/spi/delivery.js +18 -0
  189. package/dist/spi/delivery.js.map +1 -0
  190. package/dist/spi/handler-registry.d.ts +8 -0
  191. package/dist/spi/handler-registry.d.ts.map +1 -0
  192. package/dist/spi/handler-registry.js +15 -0
  193. package/dist/spi/handler-registry.js.map +1 -0
  194. package/dist/stand/stand.d.ts +73 -1
  195. package/dist/stand/stand.d.ts.map +1 -1
  196. package/dist/stand/stand.js +221 -48
  197. package/dist/stand/stand.js.map +1 -1
  198. package/dist/stand/subscription-observer.d.ts.map +1 -1
  199. package/dist/stand/subscription-observer.js +5 -12
  200. package/dist/stand/subscription-observer.js.map +1 -1
  201. package/dist/stand/subscription-registry.js +1 -1
  202. package/dist/stand/subscription-registry.js.map +1 -1
  203. package/dist/testing/index.d.ts +25 -3
  204. package/dist/testing/index.d.ts.map +1 -1
  205. package/dist/testing/index.js +23 -3
  206. package/dist/testing/index.js.map +1 -1
  207. package/dist/tsconfig.tsbuildinfo +1 -1
  208. package/package.json +33 -23
  209. package/dist/handler/build-time-handler-analyzer.d.ts +0 -150
  210. package/dist/handler/build-time-handler-analyzer.d.ts.map +0 -1
  211. package/dist/handler/build-time-handler-analyzer.js +0 -1101
  212. package/dist/handler/build-time-handler-analyzer.js.map +0 -1
  213. package/dist/handler/generated-registry-writer.d.ts +0 -58
  214. package/dist/handler/generated-registry-writer.d.ts.map +0 -1
  215. package/dist/handler/generated-registry-writer.js +0 -458
  216. package/dist/handler/generated-registry-writer.js.map +0 -1
  217. package/dist/handler/handler-codegen.d.ts +0 -48
  218. package/dist/handler/handler-codegen.d.ts.map +0 -1
  219. package/dist/handler/handler-codegen.js +0 -214
  220. package/dist/handler/handler-codegen.js.map +0 -1
  221. package/dist/server/browser-server.d.ts +0 -49
  222. package/dist/server/browser-server.d.ts.map +0 -1
  223. package/dist/server/browser-server.js.map +0 -1
  224. package/dist/server/durable-subscription-bindings.d.ts.map +0 -1
  225. package/dist/server/durable-subscription-bindings.js.map +0 -1
  226. /package/dist/{server → browser}/durable-subscription-bindings.d.ts +0 -0
  227. /package/dist/{server → browser}/durable-subscription-bindings.js +0 -0
@@ -26,11 +26,15 @@ import { EntityStateUpdateSchema, EntityUpdatesSchema, EventUpdatesSchema, Subsc
26
26
  import { SubscriptionService } from "@spine-event-engine/proto/client";
27
27
  import { AckSchema } from "@spine-event-engine/proto";
28
28
  import { ResponseSchema, StatusSchema, } from "@spine-event-engine/proto";
29
- import { RecordMask, TenantBoundary, } from "@spine-event-engine/storage";
29
+ import {} from "@spine-event-engine/storage";
30
+ import { TenantBoundary } from "@spine-event-engine/storage/provider";
30
31
  import { boundedContextAccess } from "../context/bounded-context.js";
32
+ import { EffectiveTenants } from "../context/effective-tenant.js";
31
33
  import { CommandValidationError } from "../bus/command-errors.js";
32
34
  import { TransitionValidationError } from "../repository/command-errors.js";
33
35
  import {} from "../stand/stand.js";
36
+ import { QueryReader } from "./query-reader.js";
37
+ import { RegisteredTargets } from "./registered-targets.js";
34
38
  import { emitServerWarning } from "../server/server-log.js";
35
39
  import { managedChildSubscriptionAccess } from "../server/managed-child-subscription.js";
36
40
  import { InMemorySubscriptionRegistry, } from "../stand/subscription-registry.js";
@@ -40,16 +44,19 @@ const serviceInstances = new WeakSet();
40
44
  * Small route registrar for the first public Spine gRPC service slice.
41
45
  *
42
46
  * `QueryService.Read` supports ID filters for any registered state route,
43
- * projection-state `include_all = true` reads, top-level equality filters over
44
- * declared projection `(column)` proto field names, field masks, ordering by
45
- * declared proto columns, and bounded result sets. An absent response format or
46
- * zero wire limit receives an implicit 1,000-row cap without requiring
47
- * ordering; a positive limit requires at least one ordering directive. Use
48
- * proto column names such as `open_task_count`, not generated TS local names
49
- * such as `openTaskCount`; undeclared columns return stable `INVALID_QUERY`
50
- * responses before Stand storage is read. Unsupported query operators and
51
- * shapes also return stable `INVALID_QUERY` responses before Stand storage is
52
- * read.
47
+ * projection-state `include_all = true` reads, and column filters with
48
+ * `EQUAL`, `GREATER_THAN`, `LESS_THAN`, `GREATER_OR_EQUAL`, or `LESS_OR_EQUAL`
49
+ * comparisons inside nested `ALL`/`EITHER` composites. Filter paths name one
50
+ * declared `(column)` proto field or supported system column, and values must
51
+ * match its type. Range comparisons and ordering require a string or numeric
52
+ * scalar column, `Timestamp`, or `version`; ordering accepts only `ASCENDING`
53
+ * or `DESCENDING`. An absent response format or zero
54
+ * wire limit receives an implicit 1,000-row cap without requiring ordering; a
55
+ * positive limit of at most 1,000 requires at least one ordering directive.
56
+ * Queries allow at most 100 IDs, 16 simple filters, 8 composites, and 8 orderings.
57
+ * Use proto column names such as `open_task_count`, not generated TS local names
58
+ * such as `openTaskCount`. Invalid columns, values, operators, and query shapes
59
+ * return stable `INVALID_QUERY` responses before Stand storage is read.
53
60
  *
54
61
  * `SubscriptionService.Subscribe` accepts known state targets with
55
62
  * `include_all` or validated ID/field filters and known event targets exposed
@@ -59,8 +66,8 @@ const serviceInstances = new WeakSet();
59
66
  * subscriptions attach to `Stand`, while
60
67
  * event subscriptions attach to a framework-internal `EventBus` listener.
61
68
  * Filtered state topics deliver matching states, emit `no_longer_matching`
62
- * when previous state matched and new state does not, and apply topic masks
63
- * only to delivered states. Event topics stream wire-level `event_updates`
69
+ * when previous state matched and new state does not, and deliver complete states.
70
+ * Event topics stream wire-level `event_updates`
64
71
  * containing cloned framework `Event` envelopes; rejection updates omit
65
72
  * rejected-command payload forms and stack trace from their client-facing
66
73
  * context.
@@ -91,31 +98,19 @@ export class SpineServices {
91
98
  this.#contexts = Object.freeze([...options.contexts]);
92
99
  this.#queueLimit = ServiceValues.positiveInteger(options.queueLimit ?? ServiceValues.defaultQueueLimit);
93
100
  this.#subscriptionLimit = ServiceValues.subscriptionLimit(options.subscriptionLimit ?? ServiceValues.defaultSubscriptionLimit);
101
+ this.#registerSignalRoutes();
102
+ this.#registerStateRoutes(new RegisteredTargets(this.#contexts));
103
+ }
104
+ /**
105
+ * Registers Command and Event routes exposed by the assembled contexts.
106
+ */
107
+ #registerSignalRoutes() {
94
108
  for (const context of this.#contexts) {
95
109
  for (const typeUrl of context.commandBus().acceptedCommandTypes()) {
96
110
  if (!this.#commandRoutes.has(typeUrl)) {
97
111
  this.#commandRoutes.set(typeUrl, { context, typeUrl });
98
112
  }
99
113
  }
100
- for (const repository of context.registeredRepositories()) {
101
- const schema = repository.stateSchema;
102
- const typeUrl = TypeUrls.derive(schema);
103
- const declaredColumns = repository.metadata.columns.map((column) => column.name);
104
- const systemColumns = ["version", "archived", "deleted"];
105
- this.#stateRoutes.set(typeUrl, {
106
- allowedColumnNames: new Set([...declaredColumns, ...systemColumns]),
107
- columnFields: new Map(repository.metadata.columns.flatMap((column) => {
108
- const field = schema.fields.find((candidate) => candidate.name === column.name);
109
- return field === undefined ? [] : [[column.name, field]];
110
- })),
111
- context,
112
- entityFamily: repository.entityFamily,
113
- idField: ServiceValues.stateRouteIdField(schema, repository.idField),
114
- kind: "state",
115
- schema,
116
- typeUrl,
117
- });
118
- }
119
114
  for (const typeUrl of context.eventBus().acceptedEventTypes()) {
120
115
  if (!this.#eventRoutes.has(typeUrl)) {
121
116
  this.#eventRoutes.set(typeUrl, { context, kind: "event", typeUrl });
@@ -123,6 +118,31 @@ export class SpineServices {
123
118
  }
124
119
  }
125
120
  }
121
+ /**
122
+ * Registers state routes from the validated server-wide Entity lookup.
123
+ *
124
+ * @param targets Validated state registrations.
125
+ */
126
+ #registerStateRoutes(targets) {
127
+ for (const { context, repository, typeUrl } of targets.all()) {
128
+ const schema = repository.stateSchema;
129
+ const declaredColumns = repository.metadata.columns.map((column) => column.name);
130
+ const systemColumns = ["version", "archived", "deleted"];
131
+ this.#stateRoutes.set(typeUrl, {
132
+ allowedColumnNames: new Set([...declaredColumns, ...systemColumns]),
133
+ columnFields: new Map(repository.metadata.columns.flatMap((column) => {
134
+ const field = schema.fields.find((candidate) => candidate.name === column.name);
135
+ return field === undefined ? [] : [[column.name, field]];
136
+ })),
137
+ context,
138
+ entityFamily: repository.entityFamily,
139
+ idField: ServiceValues.stateRouteIdField(schema, repository.idField),
140
+ kind: "state",
141
+ schema,
142
+ typeUrl,
143
+ });
144
+ }
145
+ }
126
146
  /**
127
147
  * Registers CommandService, QueryService, and SubscriptionService routes.
128
148
  *
@@ -143,6 +163,12 @@ export class SpineServices {
143
163
  });
144
164
  return router;
145
165
  }
166
+ /**
167
+ * Resolves a public Command route before attempting dispatch.
168
+ *
169
+ * @param command Public Command request.
170
+ * @returns Acknowledgement for route validation or dispatch.
171
+ */
146
172
  async #post(command) {
147
173
  const messageId = command.id && AnyMessages.pack(CommandIdSchema, command.id, { validate: false });
148
174
  const typeUrl = command.message?.typeUrl;
@@ -181,6 +207,12 @@ export class SpineServices {
181
207
  });
182
208
  }
183
209
  }
210
+ /**
211
+ * Validates a public state query before reading its registered Stand.
212
+ *
213
+ * @param query Public query request.
214
+ * @returns States or a stable query error response.
215
+ */
184
216
  async #read(query) {
185
217
  const target = query.target;
186
218
  const route = this.#readRoute(target);
@@ -191,13 +223,26 @@ export class SpineServices {
191
223
  if (queryError !== undefined) {
192
224
  return ServiceValues.queryErrorResponse(queryError.type, queryError.message);
193
225
  }
226
+ return this.#readValidated(query, target, route);
227
+ }
228
+ /**
229
+ * Resolves the effective tenant and executes an already validated read.
230
+ *
231
+ * @param query Validated public query.
232
+ * @param target Registered query target.
233
+ * @param route Route containing the target Stand and schema.
234
+ * @returns States or a stable tenant/read error response.
235
+ */
236
+ async #readValidated(query, target, route) {
194
237
  const tenantId = ServiceValues.tenantValue(query.context?.tenantId);
195
238
  const tenantError = ServiceValues.tenantMismatch(route.context.isMultitenant, tenantId, "query");
196
239
  if (tenantError !== undefined) {
197
240
  return ServiceValues.queryErrorResponse(tenantError.type, tenantError.message);
198
241
  }
199
242
  try {
200
- return await this.#query(route, ServiceValues.createReadPlan(target, query, route), tenantId);
243
+ const effective = EffectiveTenants.current(route.context.isMultitenant, tenantId);
244
+ const destination = EffectiveTenants.destination(effective, route.context.isMultitenant);
245
+ return await this.#query(route, ServiceValues.createReadPlan(target, query, route), destination);
201
246
  }
202
247
  catch {
203
248
  return ServiceValues.queryErrorResponse("QUERY_READ_ERROR", "Query read failed.");
@@ -207,13 +252,7 @@ export class SpineServices {
207
252
  return target === undefined ? undefined : this.#stateRoutes.get(target.type);
208
253
  }
209
254
  async #query(route, plan, tenantId) {
210
- const boundedPlan = {
211
- ...plan,
212
- candidateLimit: ServiceValues.queryResultLimit,
213
- };
214
- const results = await route.context
215
- .stand()
216
- .queryPlanVersioned(route.schema, boundedPlan, ServiceValues.tenantOptions(tenantId));
255
+ const results = await QueryReader.read(route.context.stand(), route.schema, plan, tenantId, ServiceValues.queryResultLimit);
217
256
  return create(QueryResponseSchema, {
218
257
  response: ServiceValues.okResponse(),
219
258
  message: results.map((result) => ServiceValues.packVersionedState(route.schema, result)),
@@ -501,12 +540,23 @@ export class SpineServices {
501
540
  * @internal
502
541
  */
503
542
  export const spineServicesAccess = Object.freeze({
543
+ /**
544
+ * Clears the logger installed for a closed service adapter.
545
+ *
546
+ * @param services Adapter whose logger is removed.
547
+ */
504
548
  clearLogger(services) {
505
549
  if (!serviceInstances.has(services)) {
506
550
  throw new TypeError("SpineServices logger requires a SpineServices instance.");
507
551
  }
508
552
  serviceLoggers.delete(services);
509
553
  },
554
+ /**
555
+ * Sets the server logger for service diagnostics.
556
+ *
557
+ * @param services Adapter receiving the logger.
558
+ * @param logger Server logging layer.
559
+ */
510
560
  installLogger(services, logger) {
511
561
  if (!serviceInstances.has(services)) {
512
562
  throw new TypeError("SpineServices logger requires a SpineServices instance.");
@@ -514,21 +564,44 @@ export const spineServicesAccess = Object.freeze({
514
564
  serviceLoggers.set(services, logger);
515
565
  },
516
566
  });
567
+ /**
568
+ * Buffers subscription updates and wakes active stream readers.
569
+ */
517
570
  class SubscriptionDelivery {
518
571
  #queue = [];
519
572
  #waiters = [];
520
573
  #queueLimit;
521
574
  #subscription;
522
575
  #closed = false;
576
+ /**
577
+ * Sets the bounded update queue capacity.
578
+ *
579
+ * @param queueLimit Maximum pending updates before closure.
580
+ */
523
581
  constructor(queueLimit) {
524
582
  this.#queueLimit = queueLimit;
525
583
  }
584
+ /**
585
+ * Checks whether a source attachment is active.
586
+ *
587
+ * @returns `true` after an attachment is installed.
588
+ */
526
589
  get active() {
527
590
  return this.#subscription !== undefined;
528
591
  }
592
+ /**
593
+ * Checks whether this delivery queue has closed.
594
+ *
595
+ * @returns `true` after closure.
596
+ */
529
597
  get closed() {
530
598
  return this.#closed;
531
599
  }
600
+ /**
601
+ * Attaches a source subscription or immediately removes it after close.
602
+ *
603
+ * @param subscription Source attachment to track.
604
+ */
532
605
  attach(subscription) {
533
606
  if (this.#closed) {
534
607
  subscription.unsubscribe();
@@ -536,6 +609,11 @@ class SubscriptionDelivery {
536
609
  }
537
610
  this.#subscription = subscription;
538
611
  }
612
+ /**
613
+ * Queues one update or wakes the next waiting stream reader.
614
+ *
615
+ * @param update State or Event update to deliver.
616
+ */
539
617
  push(update) {
540
618
  if (this.#closed) {
541
619
  return;
@@ -552,6 +630,11 @@ class SubscriptionDelivery {
552
630
  waiter(update);
553
631
  }
554
632
  }
633
+ /**
634
+ * Reads the next queued update, waiting while the stream is open and empty.
635
+ *
636
+ * @returns Next update, or `undefined` after closure.
637
+ */
555
638
  next() {
556
639
  const update = this.#queue.shift();
557
640
  if (update !== undefined || this.#closed) {
@@ -559,6 +642,9 @@ class SubscriptionDelivery {
559
642
  }
560
643
  return new Promise((resolve) => this.#waiters.push(resolve));
561
644
  }
645
+ /**
646
+ * Closes the source subscription and completes all waiting readers.
647
+ */
562
648
  close() {
563
649
  if (this.#closed) {
564
650
  return;
@@ -573,1014 +659,1006 @@ class SubscriptionDelivery {
573
659
  }
574
660
  }
575
661
  /**
576
- * Groups non-public service contract, query, and subscription operations.
662
+ * Builds an OK status for service responses.
577
663
  */
578
- const ServiceValues = (() => {
579
- function okStatus() {
580
- return create(StatusSchema, {
581
- status: {
582
- case: "ok",
583
- value: create(EmptySchema),
584
- },
585
- });
586
- }
587
- function errorStatus(type, message, details) {
588
- return create(StatusSchema, {
589
- status: {
590
- case: "error",
591
- value: create(ErrorSchema, {
592
- type,
593
- message,
594
- ...(details === undefined ? {} : { details }),
595
- }),
596
- },
597
- });
598
- }
599
- function okResponse() {
600
- return create(ResponseSchema, { status: okStatus() });
664
+ function okStatus() {
665
+ return create(StatusSchema, {
666
+ status: {
667
+ case: "ok",
668
+ value: create(EmptySchema),
669
+ },
670
+ });
671
+ }
672
+ function errorStatus(type, message, details) {
673
+ return create(StatusSchema, {
674
+ status: {
675
+ case: "error",
676
+ value: create(ErrorSchema, {
677
+ type,
678
+ message,
679
+ ...(details === undefined ? {} : { details }),
680
+ }),
681
+ },
682
+ });
683
+ }
684
+ function okResponse() {
685
+ return create(ResponseSchema, { status: okStatus() });
686
+ }
687
+ function errorResponse(type, message) {
688
+ return create(ResponseSchema, { status: errorStatus(type, message) });
689
+ }
690
+ function queryErrorResponse(type, message) {
691
+ return create(QueryResponseSchema, {
692
+ response: errorResponse(type, message),
693
+ });
694
+ }
695
+ function validateReadQuery(query, target, route) {
696
+ const formatError = formatReadError(query.format, route);
697
+ if (formatError !== undefined) {
698
+ return formatError;
699
+ }
700
+ switch (target.criterion.case) {
701
+ case "includeAll":
702
+ if (!target.criterion.value) {
703
+ return invalidCriterionError();
704
+ }
705
+ return undefined;
706
+ case "filters":
707
+ return validateFilters(target.criterion.value, route);
708
+ default:
709
+ return invalidCriterionError();
601
710
  }
602
- function errorResponse(type, message) {
603
- return create(ResponseSchema, { status: errorStatus(type, message) });
711
+ }
712
+ function validateFilters(filters, route) {
713
+ const idCount = filters.idFilter?.id.length ?? 0;
714
+ if (idCount > QueryLimits.idCount) {
715
+ return invalidQueryError("QueryService.Read id_filter may contain at most 100 IDs.");
604
716
  }
605
- function queryErrorResponse(type, message) {
606
- return create(QueryResponseSchema, {
607
- response: errorResponse(type, message),
608
- });
717
+ const idEntries = filters.idFilter?.id;
718
+ if (idEntries?.some((id) => id === undefined) === true) {
719
+ return invalidQueryError("QueryService.Read id_filter entries are required.");
609
720
  }
610
- function validateReadQuery(query, target, route) {
611
- const formatError = formatReadError(query.format, route);
612
- if (formatError !== undefined) {
613
- return formatError;
614
- }
615
- switch (target.criterion.case) {
616
- case "includeAll":
617
- if (!target.criterion.value) {
618
- return invalidCriterionError();
619
- }
620
- return undefined;
621
- case "filters":
622
- return validateFilters(target.criterion.value, route);
623
- default:
624
- return invalidCriterionError();
625
- }
721
+ const idError = validateQueryIds(idEntries, route);
722
+ if (idError !== undefined) {
723
+ return idError;
626
724
  }
627
- function validateFilters(filters, route) {
628
- const idCount = filters.idFilter?.id.length ?? 0;
629
- if (idCount > QueryLimits.idCount) {
630
- return invalidQueryError("QueryService.Read id_filter may contain at most 100 IDs.");
631
- }
632
- const idEntries = filters.idFilter?.id;
633
- if (idEntries?.some((id) => id === undefined) === true) {
634
- return invalidQueryError("QueryService.Read id_filter entries are required.");
635
- }
636
- const idError = validateQueryIds(idEntries, route);
637
- if (idError !== undefined) {
638
- return idError;
639
- }
640
- if (filters.filter.length > QueryLimits.compositeCount) {
641
- return invalidQueryError("QueryService.Read may contain at most 8 composite filters.");
642
- }
643
- const filterError = validateCompositeFilters(filters.filter, route);
644
- if (filterError !== undefined) {
645
- return filterError;
646
- }
647
- return idCount === 0 && countSimpleFilters(filters.filter) === 0
648
- ? {
649
- type: "INVALID_QUERY",
650
- message: "QueryService.Read requires an ID filter or column filter.",
651
- }
652
- : undefined;
725
+ if (filters.filter.length > QueryLimits.compositeCount) {
726
+ return invalidQueryError("QueryService.Read may contain at most 8 composite filters.");
653
727
  }
654
- function validateCompositeFilters(filters, route) {
655
- const pending = filters.map((filter) => ({ filter, depth: 0 }));
656
- const seen = new WeakSet();
657
- let compositeCount = 0;
658
- let simpleCount = 0;
659
- while (pending.length > 0) {
660
- const current = pending.pop();
661
- if (current === undefined)
662
- break;
663
- const filter = current.filter;
664
- if (seen.has(filter))
665
- return unsupportedFilterError("QueryService.Read filters must not contain cycles.");
666
- seen.add(filter);
667
- compositeCount += 1;
668
- simpleCount += filter.filter.length;
669
- if (filter.filter.length === 0 && filter.compositeFilter.length === 0) {
670
- return invalidQueryError("QueryService.Read composite filter must not be empty.");
671
- }
672
- if (compositeCount > QueryLimits.compositeCount || current.depth > 8) {
673
- return invalidQueryError("QueryService.Read may contain at most 8 composite filters.");
674
- }
675
- if (simpleCount > QueryLimits.filterCount) {
676
- return invalidQueryError("QueryService.Read may contain at most 16 simple column filters.");
677
- }
678
- if (compositeCount + pending.length + filter.compositeFilter.length >
679
- QueryLimits.compositeCount) {
680
- return invalidQueryError("QueryService.Read may contain at most 8 composite filters.");
681
- }
682
- if (current.depth >= 8 && filter.compositeFilter.length > 0) {
683
- return invalidQueryError("QueryService.Read may contain at most 8 composite filters.");
684
- }
685
- const operator = filter.operator;
686
- if (operator !== CompositeFilter_CompositeOperator.ALL &&
687
- operator !== CompositeFilter_CompositeOperator.EITHER) {
688
- return unsupportedFilterError("QueryService.Read composite operator must be ALL or EITHER.");
689
- }
690
- const simpleError = validateSimpleFilters(filter.filter, route);
691
- if (simpleError !== undefined) {
692
- return simpleError;
693
- }
694
- for (const child of filter.compositeFilter) {
695
- pending.push({ filter: child, depth: current.depth + 1 });
696
- }
697
- }
698
- return undefined;
728
+ const filterError = validateCompositeFilters(filters.filter, route);
729
+ if (filterError !== undefined) {
730
+ return filterError;
699
731
  }
700
- function validateQueryIds(ids, route) {
701
- const schema = route.idField.message;
702
- if (schema === undefined || ids === undefined) {
703
- return undefined;
704
- }
705
- if (ids.some((id) => id === undefined || AnyMessages.unpack(id, schema) === undefined)) {
706
- return invalidQueryError(`QueryService.Read id_filter values must pack ${schema.typeName}.`);
732
+ return idCount === 0 && countSimpleFilters(filters.filter) === 0
733
+ ? {
734
+ type: "INVALID_QUERY",
735
+ message: "QueryService.Read requires an ID filter or column filter.",
707
736
  }
737
+ : undefined;
738
+ }
739
+ function validateCompositeFilters(filters, route) {
740
+ const pending = filters.map((filter) => ({ filter, depth: 0 }));
741
+ const seen = new WeakSet();
742
+ let compositeCount = 0;
743
+ let simpleCount = 0;
744
+ while (pending.length > 0) {
745
+ const current = pending.pop();
746
+ if (current === undefined)
747
+ break;
748
+ const filter = current.filter;
749
+ if (seen.has(filter))
750
+ return unsupportedFilterError("QueryService.Read filters must not contain cycles.");
751
+ seen.add(filter);
752
+ compositeCount += 1;
753
+ simpleCount += filter.filter.length;
754
+ const error = ServiceValues.validateCompositeNode(filter, current.depth, compositeCount, simpleCount, pending.length, route);
755
+ if (error !== undefined)
756
+ return error;
757
+ for (const child of filter.compositeFilter) {
758
+ pending.push({ filter: child, depth: current.depth + 1 });
759
+ }
760
+ }
761
+ return undefined;
762
+ }
763
+ function validateQueryIds(ids, route) {
764
+ const schema = route.idField.message;
765
+ if (schema === undefined || ids === undefined) {
708
766
  return undefined;
709
767
  }
710
- function countSimpleFilters(filters) {
711
- const pending = [...filters];
712
- let count = 0;
713
- while (pending.length > 0) {
714
- const filter = pending.pop();
715
- if (filter === undefined)
716
- break;
717
- count += filter.filter.length;
718
- pending.push(...filter.compositeFilter);
719
- }
720
- return count;
721
- }
722
- function validateSimpleFilters(filters, route) {
723
- for (const filter of filters) {
724
- if (wireComparison(filter.operator) === undefined) {
725
- return unsupportedFilterError("QueryService.Read comparison operator is not supported.");
726
- }
727
- const column = filter.fieldPath?.fieldName[0];
728
- if (column === undefined || column.trim().length === 0) {
729
- return unsupportedFilterError("QueryService.Read column filter field is required.");
730
- }
731
- if (filter.fieldPath?.fieldName.length !== 1) {
732
- return unsupportedFilterError("QueryService.Read supports only top-level column filters.");
733
- }
734
- if (!route.allowedColumnNames.has(column)) {
735
- return unsupportedFilterError(`QueryService.Read column filter "${column}" is not a declared column.`);
736
- }
737
- if (filter.value === undefined) {
738
- return unsupportedFilterError("QueryService.Read column filter value is required.");
739
- }
740
- const field = route.columnFields.get(column);
741
- if (field === undefined
742
- ? !systemValueMatches(column, filter.value)
743
- : !filterValueMatches(field, filter.value)) {
744
- return unsupportedFilterError(`QueryService.Read column filter "${column}" has the wrong value type.`);
745
- }
746
- if (filter.operator !== Filter_Operator.EQUAL &&
747
- (field === undefined ? column !== "version" : !supportsRange(field))) {
748
- return unsupportedFilterError(`QueryService.Read column filter "${column}" does not support range comparison.`);
749
- }
750
- }
751
- return undefined;
768
+ if (ids.some((id) => id === undefined || AnyMessages.unpack(id, schema) === undefined)) {
769
+ return invalidQueryError(`QueryService.Read id_filter values must pack ${schema.typeName}.`);
752
770
  }
753
- function formatReadError(format, route) {
754
- if (format === undefined) {
755
- return undefined;
756
- }
757
- if (format.orderBy.length > QueryLimits.orderCount) {
758
- return unsupportedFormatError("QueryService.Read order_by may contain at most 8 entries.");
759
- }
760
- if ((format.fieldMask?.paths.length ?? 0) > QueryLimits.maskPathCount) {
761
- return unsupportedFormatError("QueryService.Read field_mask may contain at most 32 paths.");
762
- }
763
- if (format.fieldMask?.paths.some((path) => path.length > QueryLimits.maskPathLength) ?? false) {
764
- return unsupportedFormatError("QueryService.Read field_mask paths may contain at most 128 characters.");
765
- }
766
- for (const path of format.fieldMask?.paths ?? []) {
767
- if (resolveQueryMask(route.schema, path) === undefined) {
768
- return unsupportedFormatError(`QueryService.Read field_mask path "${path}" is not a state field.`);
769
- }
771
+ return undefined;
772
+ }
773
+ function countSimpleFilters(filters) {
774
+ const pending = [...filters];
775
+ let count = 0;
776
+ while (pending.length > 0) {
777
+ const filter = pending.pop();
778
+ if (filter === undefined)
779
+ break;
780
+ count += filter.filter.length;
781
+ pending.push(...filter.compositeFilter);
782
+ }
783
+ return count;
784
+ }
785
+ function validateSimpleFilters(filters, route) {
786
+ for (const filter of filters) {
787
+ if (wireComparison(filter.operator) === undefined) {
788
+ return unsupportedFilterError("QueryService.Read comparison operator is not supported.");
770
789
  }
771
- if (format.limit > QueryLimits.resultCount) {
772
- return {
773
- type: "INVALID_QUERY",
774
- message: "QueryService.Read limit may be at most 1000.",
775
- };
790
+ const column = filter.fieldPath?.fieldName[0];
791
+ if (column === undefined || column.trim().length === 0) {
792
+ return unsupportedFilterError("QueryService.Read column filter field is required.");
776
793
  }
777
- if (format.limit > 0 && format.orderBy.length === 0) {
778
- return {
779
- type: "INVALID_QUERY",
780
- message: "QueryService.Read limit requires ordering.",
781
- };
794
+ if (filter.fieldPath?.fieldName.length !== 1) {
795
+ return unsupportedFilterError("QueryService.Read supports only top-level column filters.");
782
796
  }
783
- for (const order of format.orderBy) {
784
- if (order.column.trim().length === 0) {
785
- return unsupportedFormatError("QueryService.Read order_by column is required.");
786
- }
787
- if (!route.allowedColumnNames.has(order.column)) {
788
- return unsupportedFormatError(`QueryService.Read order_by column "${order.column}" is not a declared column.`);
789
- }
790
- const field = route.columnFields.get(order.column);
791
- if (field === undefined ? order.column !== "version" : !supportsRange(field)) {
792
- return unsupportedFormatError(`QueryService.Read order_by column "${order.column}" is not orderable.`);
793
- }
794
- if (order.direction !== OrderBy_Direction.ASCENDING &&
795
- order.direction !== OrderBy_Direction.DESCENDING) {
796
- return unsupportedFormatError("QueryService.Read order_by direction must be ASCENDING or DESCENDING.");
797
- }
797
+ if (!route.allowedColumnNames.has(column)) {
798
+ return unsupportedFilterError(`QueryService.Read column filter "${column}" is not a declared column.`);
798
799
  }
799
- return undefined;
800
- }
801
- function createReadPlan(target, query, route) {
802
- const predicate = target.criterion.case === "filters"
803
- ? normalizedFilters(target.criterion.value, route)
804
- : undefined;
805
- const format = query.format;
806
- return Object.freeze({
807
- ...(predicate === undefined ? {} : { predicate }),
808
- ...(format?.fieldMask === undefined
809
- ? {}
810
- : {
811
- mask: {
812
- paths: format.fieldMask.paths.map((path) => requiredQueryMask(route.schema, path)),
813
- },
814
- }),
815
- ...(format === undefined || format.orderBy.length === 0
816
- ? {}
817
- : {
818
- order: format.orderBy.map((order) => ({
819
- column: order.column,
820
- direction: order.direction === OrderBy_Direction.DESCENDING
821
- ? "desc"
822
- : "asc",
823
- })),
824
- }),
825
- ...(format === undefined || format.limit === 0 ? {} : { limit: format.limit }),
826
- });
827
- }
828
- function normalizedFilters(filters, route) {
829
- const predicates = [];
830
- if (filters.idFilter !== undefined) {
831
- predicates.push({
832
- kind: "ids",
833
- ids: filters.idFilter.id.map((id) => decodeQueryIdValue(id, route)),
834
- });
835
- }
836
- predicates.push(...filters.filter.map((filter) => normalizedComposite(filter, route)));
837
- const onlyPredicate = predicates[0];
838
- return predicates.length === 1 && onlyPredicate !== undefined
839
- ? onlyPredicate
840
- : { kind: "all", predicates };
841
- }
842
- function decodeQueryIdValue(value, route) {
843
- const schema = route.idField.message;
844
- return schema === undefined ? decodeAnyValue(value) : AnyMessages.unpack(value, schema);
845
- }
846
- function normalizedComposite(filter, route) {
847
- const predicates = [
848
- ...filter.filter.map((child) => ({
849
- kind: "comparison",
850
- column: requiredFilterColumn(child),
851
- operator: requiredComparison(child),
852
- value: decodeColumnValue(child, route),
853
- })),
854
- ...filter.compositeFilter.map((child) => normalizedComposite(child, route)),
855
- ];
856
- return {
857
- kind: filter.operator === CompositeFilter_CompositeOperator.EITHER ? "either" : "all",
858
- predicates,
859
- };
860
- }
861
- function requiredFilterColumn(filter) {
862
- const column = filter.fieldPath?.fieldName[0];
863
- if (column !== undefined)
864
- return column;
865
- throw new TypeError("Validated query filter has no column.");
866
- }
867
- function requiredComparison(filter) {
868
- const operator = wireComparison(filter.operator);
869
- if (operator !== undefined)
870
- return operator;
871
- throw new TypeError("Validated query filter has no supported comparison operator.");
872
- }
873
- function decodeColumnValue(filter, route) {
874
- const column = filter.fieldPath?.fieldName[0] ?? "";
875
- if (column === "version" && filter.value !== undefined) {
876
- const value = AnyMessages.unpack(filter.value, VersionSchema);
877
- if (value !== undefined)
878
- return value;
879
- throw new TypeError('Validated query column "version" has an invalid value.');
880
- }
881
- if ((column === "archived" || column === "deleted") && filter.value !== undefined) {
882
- const value = AnyMessages.unpack(filter.value, BoolValueSchema);
883
- if (value !== undefined)
884
- return value.value;
885
- throw new TypeError(`Validated query column "${column}" has an invalid value.`);
800
+ if (filter.value === undefined) {
801
+ return unsupportedFilterError("QueryService.Read column filter value is required.");
886
802
  }
887
803
  const field = route.columnFields.get(column);
888
- return decodeAnyValue(filter.value, field?.fieldKind === "message" ? field.message : undefined);
889
- }
890
- function filterValueMatches(field, value) {
891
- const schema = filterValueSchema(field);
892
- if (schema === undefined || value.typeUrl !== TypeUrls.derive(schema))
893
- return false;
894
- const decoded = field.fieldKind === "message" ? decodeAnyValue(value, schema) : decodeAnyValue(value);
895
- if (field.fieldKind === "message") {
896
- return isMessage(decoded) && decoded.$typeName === field.message.typeName;
897
- }
898
- if (field.fieldKind === "enum")
899
- return typeof decoded === "number" && Number.isInteger(decoded);
900
- if (field.fieldKind !== "scalar")
901
- return false;
902
- if (field.scalar === ScalarType.BOOL)
903
- return typeof decoded === "boolean";
904
- if (field.scalar === ScalarType.BYTES)
905
- return decoded instanceof Uint8Array;
906
- if (field.scalar === ScalarType.STRING)
907
- return typeof decoded === "string";
908
- if (field.scalar === ScalarType.INT64 ||
909
- field.scalar === ScalarType.UINT64 ||
910
- field.scalar === ScalarType.SFIXED64 ||
911
- field.scalar === ScalarType.FIXED64 ||
912
- field.scalar === ScalarType.SINT64) {
913
- return typeof decoded === "bigint";
914
- }
915
- return typeof decoded === "number" && Number.isFinite(decoded);
916
- }
917
- function filterValueSchema(field) {
918
- if (field.fieldKind === "message")
919
- return field.message;
920
- if (field.fieldKind === "enum")
921
- return Int32ValueSchema;
922
- if (field.fieldKind !== "scalar")
923
- return undefined;
924
- if (field.scalar === ScalarType.BOOL)
925
- return BoolValueSchema;
926
- if (field.scalar === ScalarType.BYTES)
927
- return BytesValueSchema;
928
- if (field.scalar === ScalarType.DOUBLE)
929
- return DoubleValueSchema;
930
- if (field.scalar === ScalarType.FLOAT)
931
- return FloatValueSchema;
932
- if (field.scalar === ScalarType.INT64 ||
933
- field.scalar === ScalarType.SFIXED64 ||
934
- field.scalar === ScalarType.SINT64) {
935
- return Int64ValueSchema;
936
- }
937
- if (field.scalar === ScalarType.UINT64 || field.scalar === ScalarType.FIXED64) {
938
- return UInt64ValueSchema;
939
- }
940
- if (field.scalar === ScalarType.UINT32 || field.scalar === ScalarType.FIXED32) {
941
- return UInt32ValueSchema;
942
- }
943
- if (field.scalar === ScalarType.STRING)
944
- return StringValueSchema;
945
- return Int32ValueSchema;
946
- }
947
- function systemValueMatches(column, value) {
948
- if (column === "version") {
949
- return (value.typeUrl === TypeUrls.derive(VersionSchema) &&
950
- AnyMessages.unpack(value, VersionSchema) !== undefined);
804
+ if (field === undefined
805
+ ? !systemValueMatches(column, filter.value)
806
+ : !filterValueMatches(field, filter.value)) {
807
+ return unsupportedFilterError(`QueryService.Read column filter "${column}" has the wrong value type.`);
951
808
  }
952
- if (column === "archived" || column === "deleted") {
953
- return (value.typeUrl === TypeUrls.derive(BoolValueSchema) &&
954
- AnyMessages.unpack(value, BoolValueSchema) !== undefined);
809
+ if (filter.operator !== Filter_Operator.EQUAL &&
810
+ (field === undefined ? column !== "version" : !supportsRange(field))) {
811
+ return unsupportedFilterError(`QueryService.Read column filter "${column}" does not support range comparison.`);
955
812
  }
956
- return false;
957
813
  }
958
- function supportsRange(field) {
959
- if (field.fieldKind === "message") {
960
- return (field.message.typeName === "google.protobuf.Timestamp" ||
961
- field.message.typeName === "spine.core.Version");
962
- }
963
- return (field.fieldKind === "scalar" &&
964
- field.scalar !== ScalarType.BOOL &&
965
- field.scalar !== ScalarType.BYTES);
966
- }
967
- function wireComparison(operator) {
968
- if (operator === Filter_Operator.EQUAL)
969
- return "equal";
970
- if (operator === Filter_Operator.GREATER_THAN)
971
- return "greaterThan";
972
- if (operator === Filter_Operator.LESS_THAN)
973
- return "lessThan";
974
- if (operator === Filter_Operator.GREATER_OR_EQUAL)
975
- return "greaterOrEqual";
976
- if (operator === Filter_Operator.LESS_OR_EQUAL)
977
- return "lessOrEqual";
814
+ return undefined;
815
+ }
816
+ /**
817
+ * Checks query ordering and limits against the registered state route.
818
+ */
819
+ function formatReadError(format, route) {
820
+ if (format === undefined) {
978
821
  return undefined;
979
822
  }
980
- function resolveQueryMask(schema, path) {
981
- let current = schema;
982
- const local = [];
983
- for (const segment of path.split(".")) {
984
- const field = findMessageField(current, segment);
985
- if (field === undefined)
986
- return undefined;
987
- local.push(field.localName);
988
- current = field.message;
989
- }
990
- return local.join(".");
991
- }
992
- function requiredQueryMask(schema, path) {
993
- const resolved = resolveQueryMask(schema, path);
994
- if (resolved !== undefined)
995
- return resolved;
996
- throw new TypeError(`Validated query mask path "${path}" is invalid.`);
823
+ if (format.orderBy.length > QueryLimits.orderCount) {
824
+ return unsupportedFormatError("QueryService.Read order_by may contain at most 8 entries.");
997
825
  }
998
- function unsupportedFilterError(message) {
999
- return invalidQueryError(message);
1000
- }
1001
- function unsupportedFormatError(message) {
1002
- return invalidQueryError(message);
1003
- }
1004
- function invalidQueryError(message) {
826
+ if (format.limit > QueryLimits.resultCount) {
1005
827
  return {
1006
828
  type: "INVALID_QUERY",
1007
- message,
829
+ message: "QueryService.Read limit may be at most 1000.",
1008
830
  };
1009
831
  }
1010
- function invalidCriterionError() {
832
+ if (format.limit > 0 && format.orderBy.length === 0) {
1011
833
  return {
1012
834
  type: "INVALID_QUERY",
1013
- message: "QueryService.Read requires filters or include_all = true.",
835
+ message: "QueryService.Read limit requires ordering.",
1014
836
  };
1015
837
  }
1016
- const DEFAULT_QUEUE_LIMIT = 100;
1017
- const DEFAULT_SUBSCRIPTION_LIMIT = 100;
1018
- /**
1019
- * Holds bounded request limits for QueryService.
1020
- */
1021
- const QueryLimits = Object.freeze({
1022
- idCount: 100,
1023
- filterCount: 16,
1024
- compositeCount: 8,
1025
- orderCount: 8,
1026
- maskPathCount: 32,
1027
- maskPathLength: 128,
1028
- resultCount: 1_000,
1029
- });
1030
- /**
1031
- * Holds bounded request limits for SubscriptionService.
1032
- */
1033
- const SubscriptionLimits = Object.freeze({
1034
- idCount: QueryLimits.idCount,
1035
- compositeCount: 8,
1036
- filterCount: 16,
1037
- compositeDepth: 8,
1038
- maskPathCount: 32,
1039
- maskPathLength: 128,
1040
- pathComponentCount: 16,
1041
- pathSegmentLength: 128,
838
+ return ServiceValues.validateReadOrder(format.orderBy, route);
839
+ }
840
+ /**
841
+ * Converts an accepted wire query into a storage plan for complete states.
842
+ */
843
+ function createReadPlan(target, query, route) {
844
+ const predicate = target.criterion.case === "filters"
845
+ ? normalizedFilters(target.criterion.value, route)
846
+ : undefined;
847
+ const format = query.format;
848
+ return Object.freeze({
849
+ ...(predicate === undefined ? {} : { predicate }),
850
+ ...(format === undefined || format.orderBy.length === 0
851
+ ? {}
852
+ : {
853
+ order: format.orderBy.map((order) => ({
854
+ column: order.column,
855
+ direction: order.direction === OrderBy_Direction.DESCENDING
856
+ ? "desc"
857
+ : "asc",
858
+ })),
859
+ }),
860
+ ...(format === undefined || format.limit === 0 ? {} : { limit: format.limit }),
1042
861
  });
1043
- function positiveInteger(value) {
1044
- return Number.isFinite(value) && value > 0 ? Math.floor(value) : 1;
862
+ }
863
+ function normalizedFilters(filters, route) {
864
+ const predicates = [];
865
+ if (filters.idFilter !== undefined) {
866
+ predicates.push({
867
+ kind: "ids",
868
+ ids: filters.idFilter.id.map((id) => decodeQueryIdValue(id, route)),
869
+ });
1045
870
  }
1046
- function subscriptionLimit(value) {
1047
- if (!Number.isSafeInteger(value) || value <= 0) {
1048
- throw new TypeError("SpineServices subscriptionLimit must be a positive safe integer.");
1049
- }
1050
- return value;
871
+ predicates.push(...filters.filter.map((filter) => normalizedComposite(filter, route)));
872
+ const onlyPredicate = predicates[0];
873
+ return predicates.length === 1 && onlyPredicate !== undefined
874
+ ? onlyPredicate
875
+ : { kind: "all", predicates };
876
+ }
877
+ function decodeQueryIdValue(value, route) {
878
+ const schema = route.idField.message;
879
+ return schema === undefined ? decodeAnyValue(value) : AnyMessages.unpack(value, schema);
880
+ }
881
+ function normalizedComposite(filter, route) {
882
+ const predicates = [
883
+ ...filter.filter.map((child) => ({
884
+ kind: "comparison",
885
+ column: requiredFilterColumn(child),
886
+ operator: requiredComparison(child),
887
+ value: decodeColumnValue(child, route),
888
+ })),
889
+ ...filter.compositeFilter.map((child) => normalizedComposite(child, route)),
890
+ ];
891
+ return {
892
+ kind: filter.operator === CompositeFilter_CompositeOperator.EITHER ? "either" : "all",
893
+ predicates,
894
+ };
895
+ }
896
+ function requiredFilterColumn(filter) {
897
+ const column = filter.fieldPath?.fieldName[0];
898
+ if (column !== undefined)
899
+ return column;
900
+ throw new TypeError("Validated query filter has no column.");
901
+ }
902
+ function requiredComparison(filter) {
903
+ const operator = wireComparison(filter.operator);
904
+ if (operator !== undefined)
905
+ return operator;
906
+ throw new TypeError("Validated query filter has no supported comparison operator.");
907
+ }
908
+ function decodeColumnValue(filter, route) {
909
+ const column = filter.fieldPath?.fieldName[0] ?? "";
910
+ if (column === "version" && filter.value !== undefined) {
911
+ const value = AnyMessages.unpack(filter.value, VersionSchema);
912
+ if (value !== undefined)
913
+ return value;
914
+ throw new TypeError('Validated query column "version" has an invalid value.');
915
+ }
916
+ if ((column === "archived" || column === "deleted") && filter.value !== undefined) {
917
+ const value = AnyMessages.unpack(filter.value, BoolValueSchema);
918
+ if (value !== undefined)
919
+ return value.value;
920
+ throw new TypeError(`Validated query column "${column}" has an invalid value.`);
921
+ }
922
+ const field = route.columnFields.get(column);
923
+ return decodeAnyValue(filter.value, field?.fieldKind === "message" ? field.message : undefined);
924
+ }
925
+ function filterValueMatches(field, value) {
926
+ const schema = filterValueSchema(field);
927
+ if (schema === undefined || value.typeUrl !== TypeUrls.derive(schema))
928
+ return false;
929
+ const decoded = field.fieldKind === "message" ? decodeAnyValue(value, schema) : decodeAnyValue(value);
930
+ if (field.fieldKind === "message") {
931
+ return isMessage(decoded) && decoded.$typeName === field.message.typeName;
1051
932
  }
1052
- function createSubscriptionRecord(input) {
1053
- return {
1054
- id: input.id,
1055
- subscription: clone(SubscriptionSchema, input.subscription),
1056
- tenantId: input.tenantId === undefined ? undefined : clone(TenantIdSchema, input.tenantId),
1057
- delivery: new SubscriptionDelivery(input.queueLimit),
1058
- ...input.shape,
1059
- };
933
+ if (field.fieldKind === "enum")
934
+ return typeof decoded === "number" && Number.isInteger(decoded);
935
+ if (field.fieldKind !== "scalar")
936
+ return false;
937
+ if (field.scalar === ScalarType.BOOL)
938
+ return typeof decoded === "boolean";
939
+ if (field.scalar === ScalarType.BYTES)
940
+ return decoded instanceof Uint8Array;
941
+ if (field.scalar === ScalarType.STRING)
942
+ return typeof decoded === "string";
943
+ if (field.scalar === ScalarType.INT64 ||
944
+ field.scalar === ScalarType.UINT64 ||
945
+ field.scalar === ScalarType.SFIXED64 ||
946
+ field.scalar === ScalarType.FIXED64 ||
947
+ field.scalar === ScalarType.SINT64) {
948
+ return typeof decoded === "bigint";
949
+ }
950
+ return typeof decoded === "number" && Number.isFinite(decoded);
951
+ }
952
+ function filterValueSchema(field) {
953
+ if (field.fieldKind === "message")
954
+ return field.message;
955
+ if (field.fieldKind === "enum")
956
+ return Int32ValueSchema;
957
+ if (field.fieldKind !== "scalar")
958
+ return undefined;
959
+ if (field.scalar === ScalarType.BOOL)
960
+ return BoolValueSchema;
961
+ if (field.scalar === ScalarType.BYTES)
962
+ return BytesValueSchema;
963
+ if (field.scalar === ScalarType.DOUBLE)
964
+ return DoubleValueSchema;
965
+ if (field.scalar === ScalarType.FLOAT)
966
+ return FloatValueSchema;
967
+ if (field.scalar === ScalarType.INT64 ||
968
+ field.scalar === ScalarType.SFIXED64 ||
969
+ field.scalar === ScalarType.SINT64) {
970
+ return Int64ValueSchema;
971
+ }
972
+ if (field.scalar === ScalarType.UINT64 || field.scalar === ScalarType.FIXED64) {
973
+ return UInt64ValueSchema;
974
+ }
975
+ if (field.scalar === ScalarType.UINT32 || field.scalar === ScalarType.FIXED32) {
976
+ return UInt32ValueSchema;
977
+ }
978
+ if (field.scalar === ScalarType.STRING)
979
+ return StringValueSchema;
980
+ return Int32ValueSchema;
981
+ }
982
+ function systemValueMatches(column, value) {
983
+ if (column === "version") {
984
+ return (value.typeUrl === TypeUrls.derive(VersionSchema) &&
985
+ AnyMessages.unpack(value, VersionSchema) !== undefined);
1060
986
  }
1061
- function createSubscriptionRemoval() {
1062
- let reject;
1063
- let resolve;
1064
- const settled = new Promise((settle, fail) => {
1065
- reject = fail;
1066
- resolve = () => {
1067
- settle();
1068
- };
1069
- });
1070
- return { reject, resolve, settled };
987
+ if (column === "archived" || column === "deleted") {
988
+ return (value.typeUrl === TypeUrls.derive(BoolValueSchema) &&
989
+ AnyMessages.unpack(value, BoolValueSchema) !== undefined);
1071
990
  }
1072
- function uniqueContexts(contexts) {
1073
- return [...new Set(contexts)];
991
+ return false;
992
+ }
993
+ function supportsRange(field) {
994
+ if (field.fieldKind === "message") {
995
+ return (field.message.typeName === "google.protobuf.Timestamp" ||
996
+ field.message.typeName === "spine.core.Version");
997
+ }
998
+ return (field.fieldKind === "scalar" &&
999
+ field.scalar !== ScalarType.BOOL &&
1000
+ field.scalar !== ScalarType.BYTES);
1001
+ }
1002
+ function wireComparison(operator) {
1003
+ if (operator === Filter_Operator.EQUAL)
1004
+ return "equal";
1005
+ if (operator === Filter_Operator.GREATER_THAN)
1006
+ return "greaterThan";
1007
+ if (operator === Filter_Operator.LESS_THAN)
1008
+ return "lessThan";
1009
+ if (operator === Filter_Operator.GREATER_OR_EQUAL)
1010
+ return "greaterOrEqual";
1011
+ if (operator === Filter_Operator.LESS_OR_EQUAL)
1012
+ return "lessOrEqual";
1013
+ return undefined;
1014
+ }
1015
+ function unsupportedFilterError(message) {
1016
+ return invalidQueryError(message);
1017
+ }
1018
+ function unsupportedFormatError(message) {
1019
+ return invalidQueryError(message);
1020
+ }
1021
+ function invalidQueryError(message) {
1022
+ return {
1023
+ type: "INVALID_QUERY",
1024
+ message,
1025
+ };
1026
+ }
1027
+ function invalidCriterionError() {
1028
+ return {
1029
+ type: "INVALID_QUERY",
1030
+ message: "QueryService.Read requires filters or include_all = true.",
1031
+ };
1032
+ }
1033
+ const DEFAULT_QUEUE_LIMIT = 100;
1034
+ const DEFAULT_SUBSCRIPTION_LIMIT = 100;
1035
+ /**
1036
+ * Holds bounded request limits for QueryService.
1037
+ */
1038
+ const QueryLimits = Object.freeze({
1039
+ idCount: 100,
1040
+ filterCount: 16,
1041
+ compositeCount: 8,
1042
+ orderCount: 8,
1043
+ resultCount: 1_000,
1044
+ });
1045
+ /**
1046
+ * Holds bounded request limits for SubscriptionService.
1047
+ */
1048
+ const SubscriptionLimits = Object.freeze({
1049
+ idCount: QueryLimits.idCount,
1050
+ compositeCount: 8,
1051
+ filterCount: 16,
1052
+ compositeDepth: 8,
1053
+ pathComponentCount: 16,
1054
+ pathSegmentLength: 128,
1055
+ });
1056
+ function positiveInteger(value) {
1057
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : 1;
1058
+ }
1059
+ function subscriptionLimit(value) {
1060
+ if (!Number.isSafeInteger(value) || value <= 0) {
1061
+ throw new TypeError("SpineServices subscriptionLimit must be a positive safe integer.");
1074
1062
  }
1075
- function validateTopic(topic) {
1076
- if (topic.id?.value === undefined || topic.id.value.trim().length === 0) {
1077
- throw new ConnectError("Subscription topic ID is required.", Code.InvalidArgument);
1078
- }
1079
- if (topic.context === undefined) {
1080
- throw new ConnectError("Subscription topic context is required.", Code.InvalidArgument);
1081
- }
1082
- if (topic.target?.criterion.case === undefined) {
1083
- throw new ConnectError("Subscription topic criterion is required.", Code.InvalidArgument);
1084
- }
1063
+ return value;
1064
+ }
1065
+ function createSubscriptionRecord(input) {
1066
+ return {
1067
+ id: input.id,
1068
+ subscription: clone(SubscriptionSchema, input.subscription),
1069
+ tenantId: input.tenantId === undefined ? undefined : clone(TenantIdSchema, input.tenantId),
1070
+ delivery: new SubscriptionDelivery(input.queueLimit),
1071
+ ...input.shape,
1072
+ };
1073
+ }
1074
+ function createSubscriptionRemoval() {
1075
+ let reject;
1076
+ let resolve;
1077
+ const settled = new Promise((settle, fail) => {
1078
+ reject = fail;
1079
+ resolve = () => {
1080
+ settle();
1081
+ };
1082
+ });
1083
+ return { reject, resolve, settled };
1084
+ }
1085
+ function uniqueContexts(contexts) {
1086
+ return [...new Set(contexts)];
1087
+ }
1088
+ function validateTopic(topic) {
1089
+ if (topic.id?.value === undefined || topic.id.value.trim().length === 0) {
1090
+ throw new ConnectError("Subscription topic ID is required.", Code.InvalidArgument);
1085
1091
  }
1086
- function createSubscriptionShape(topic, route) {
1087
- const target = topic.target;
1088
- if (target === undefined) {
1089
- throw new ConnectError("Subscription topic target is required.", Code.InvalidArgument);
1090
- }
1091
- if (route.kind === "event") {
1092
- validateEventSubscriptionTopic(topic, target);
1093
- return { kind: "event", route };
1094
- }
1095
- const fieldMask = subscriptionFieldMask(topic, route);
1096
- switch (target.criterion.case) {
1097
- case "includeAll":
1098
- if (!target.criterion.value) {
1099
- throw new ConnectError("SubscriptionService.Subscribe requires filters or include_all = true.", Code.InvalidArgument);
1100
- }
1101
- return { kind: "state", route, matcher: { fieldMask, match: () => "state" } };
1102
- case "filters":
1103
- return {
1104
- kind: "state",
1105
- route,
1106
- matcher: createFilteredSubscriptionMatcher(target.criterion.value, route, fieldMask),
1107
- };
1108
- default:
1092
+ if (topic.context === undefined) {
1093
+ throw new ConnectError("Subscription topic context is required.", Code.InvalidArgument);
1094
+ }
1095
+ if (topic.target?.criterion.case === undefined) {
1096
+ throw new ConnectError("Subscription topic criterion is required.", Code.InvalidArgument);
1097
+ }
1098
+ }
1099
+ /**
1100
+ * Validates a topic and prepares complete state or event delivery.
1101
+ */
1102
+ function createSubscriptionShape(topic, route) {
1103
+ const target = topic.target;
1104
+ if (target === undefined) {
1105
+ throw new ConnectError("Subscription topic target is required.", Code.InvalidArgument);
1106
+ }
1107
+ if (route.kind === "event") {
1108
+ validateEventSubscriptionTopic(target);
1109
+ return { kind: "event", route };
1110
+ }
1111
+ switch (target.criterion.case) {
1112
+ case "includeAll":
1113
+ if (!target.criterion.value) {
1109
1114
  throw new ConnectError("SubscriptionService.Subscribe requires filters or include_all = true.", Code.InvalidArgument);
1110
- }
1115
+ }
1116
+ return { kind: "state", route, matcher: { match: () => "state" } };
1117
+ case "filters":
1118
+ return {
1119
+ kind: "state",
1120
+ route,
1121
+ matcher: createFilteredSubscriptionMatcher(target.criterion.value, route),
1122
+ };
1123
+ default:
1124
+ throw new ConnectError("SubscriptionService.Subscribe requires filters or include_all = true.", Code.InvalidArgument);
1111
1125
  }
1112
- function validateEventSubscriptionTopic(topic, target) {
1113
- if ((topic.fieldMask?.paths.length ?? 0) > 0) {
1114
- throw new ConnectError("SubscriptionService.Subscribe event topics do not support field_mask.", Code.InvalidArgument);
1115
- }
1116
- switch (target.criterion.case) {
1117
- case "includeAll":
1118
- if (!target.criterion.value) {
1119
- throw new ConnectError("SubscriptionService.Subscribe requires filters or include_all = true.", Code.InvalidArgument);
1120
- }
1121
- return;
1122
- case "filters":
1123
- throw new ConnectError("SubscriptionService.Subscribe event topics support only include_all in this runtime slice.", Code.InvalidArgument);
1124
- default:
1126
+ }
1127
+ /**
1128
+ * Checks the supported event topic criteria.
1129
+ */
1130
+ function validateEventSubscriptionTopic(target) {
1131
+ switch (target.criterion.case) {
1132
+ case "includeAll":
1133
+ if (!target.criterion.value) {
1125
1134
  throw new ConnectError("SubscriptionService.Subscribe requires filters or include_all = true.", Code.InvalidArgument);
1126
- }
1135
+ }
1136
+ return;
1137
+ case "filters":
1138
+ throw new ConnectError("SubscriptionService.Subscribe event topics support only include_all in this runtime slice.", Code.InvalidArgument);
1139
+ default:
1140
+ throw new ConnectError("SubscriptionService.Subscribe requires filters or include_all = true.", Code.InvalidArgument);
1127
1141
  }
1128
- function createFilteredSubscriptionMatcher(filters, route, fieldMask) {
1129
- if (filters.idFilter?.id.length === 0) {
1130
- throw new ConnectError("SubscriptionService.Subscribe id_filter requires at least one ID.", Code.InvalidArgument);
1131
- }
1132
- if (filters.idFilter !== undefined && filters.idFilter.id.length > SubscriptionLimits.idCount) {
1133
- throw new ConnectError("SubscriptionService.Subscribe id_filter may contain at most 100 IDs.", Code.InvalidArgument);
1134
- }
1135
- if (filters.idFilter === undefined && filters.filter.length === 0) {
1136
- throw new ConnectError("SubscriptionService.Subscribe requires an ID filter or field filter.", Code.InvalidArgument);
1137
- }
1138
- validateSubscriptionFilterTree(filters.filter);
1139
- const idValues = filters.idFilter?.id.map((id) => decodeSubscriptionIdValue(id, route));
1140
- const statePredicate = createSubscriptionPredicate(filters.filter, route);
1141
- return {
1142
- fieldMask,
1143
- match(update) {
1144
- if (idValues !== undefined &&
1145
- !idValues.some((id) => valuesEqual(update.id, id, route.idField.message))) {
1146
- return undefined;
1147
- }
1148
- if (statePredicate(update.state)) {
1149
- return "state";
1150
- }
1151
- if (update.previousState !== undefined && statePredicate(update.previousState)) {
1152
- return "noLongerMatching";
1153
- }
1142
+ }
1143
+ /**
1144
+ * Matches complete state updates against a topic's ID and field filters.
1145
+ */
1146
+ function createFilteredSubscriptionMatcher(filters, route) {
1147
+ ServiceValues.validateSubscriptionFilters(filters);
1148
+ const idValues = filters.idFilter?.id.map((id) => decodeSubscriptionIdValue(id, route));
1149
+ const statePredicate = createSubscriptionPredicate(filters.filter, route);
1150
+ return {
1151
+ /**
1152
+ * Matches one state update against the topic's ID and field filters.
1153
+ *
1154
+ * @param update State update to inspect.
1155
+ * @returns Delivery kind, if the update changes a matching state.
1156
+ */
1157
+ match(update) {
1158
+ if (idValues !== undefined &&
1159
+ !idValues.some((id) => valuesEqual(update.id, id, route.idField.message))) {
1154
1160
  return undefined;
1155
- },
1156
- };
1157
- }
1158
- function createSubscriptionPredicate(filters, route) {
1159
- const predicates = filters.map((filter) => createCompositePredicate(filter, route));
1160
- return (state) => predicates.every((predicate) => predicate(state));
1161
- }
1162
- function validateSubscriptionFilterTree(filters) {
1163
- if (filters.length > SubscriptionLimits.compositeCount) {
1164
- throw new ConnectError("SubscriptionService.Subscribe may contain at most 8 composite filters.", Code.InvalidArgument);
1165
- }
1166
- const stack = [];
1167
- for (const filter of filters) {
1168
- stack.push({ filter, depth: 0 });
1169
- }
1170
- const counts = {
1171
- compositeCount: 0,
1172
- simpleCount: 0,
1173
- };
1174
- while (stack.length > 0) {
1175
- const current = stack.pop();
1176
- if (current === undefined) {
1177
- continue;
1178
1161
  }
1179
- validateSubscriptionFilterNode(current.filter, current.depth, counts);
1180
- enqueueSubscriptionFilterChildren(current.filter, current.depth, stack);
1181
- }
1162
+ if (statePredicate(update.state))
1163
+ return "state";
1164
+ if (update.previousState !== undefined && statePredicate(update.previousState))
1165
+ return "noLongerMatching";
1166
+ return undefined;
1167
+ },
1168
+ };
1169
+ }
1170
+ function createSubscriptionPredicate(filters, route) {
1171
+ const predicates = filters.map((filter) => createCompositePredicate(filter, route));
1172
+ return (state) => predicates.every((predicate) => predicate(state));
1173
+ }
1174
+ function validateSubscriptionFilterTree(filters) {
1175
+ if (filters.length > SubscriptionLimits.compositeCount) {
1176
+ throw new ConnectError("SubscriptionService.Subscribe may contain at most 8 composite filters.", Code.InvalidArgument);
1177
+ }
1178
+ const stack = [];
1179
+ for (const filter of filters) {
1180
+ stack.push({ filter, depth: 0 });
1181
+ }
1182
+ const counts = {
1183
+ compositeCount: 0,
1184
+ simpleCount: 0,
1185
+ };
1186
+ while (stack.length > 0) {
1187
+ const current = stack.pop();
1188
+ if (current === undefined) {
1189
+ continue;
1190
+ }
1191
+ validateSubscriptionFilterNode(current.filter, current.depth, counts);
1192
+ enqueueSubscriptionFilterChildren(current.filter, current.depth, stack);
1182
1193
  }
1183
- function validateSubscriptionFilterNode(filter, depth, counts) {
1184
- if (depth > SubscriptionLimits.compositeDepth) {
1185
- throw new ConnectError("SubscriptionService.Subscribe composite filters may nest at most 8 levels.", Code.InvalidArgument);
1186
- }
1187
- switch (filter.operator) {
1188
- case CompositeFilter_CompositeOperator.ALL:
1189
- case CompositeFilter_CompositeOperator.EITHER:
1190
- break;
1191
- default:
1192
- throw new ConnectError("SubscriptionService.Subscribe supports only ALL or EITHER composite filters.", Code.InvalidArgument);
1193
- }
1194
- if (depth === SubscriptionLimits.compositeDepth && filter.compositeFilter.length > 0) {
1195
- throw new ConnectError("SubscriptionService.Subscribe composite filters may nest at most 8 levels.", Code.InvalidArgument);
1196
- }
1197
- counts.compositeCount += 1;
1198
- if (counts.compositeCount > SubscriptionLimits.compositeCount) {
1199
- throw new ConnectError("SubscriptionService.Subscribe may contain at most 8 composite filters.", Code.InvalidArgument);
1200
- }
1201
- counts.simpleCount += filter.filter.length;
1202
- if (counts.simpleCount > SubscriptionLimits.filterCount) {
1203
- throw new ConnectError("SubscriptionService.Subscribe may contain at most 16 simple field filters.", Code.InvalidArgument);
1204
- }
1205
- if ((filter.compositeFilter.length > 1 || depth + 1 < SubscriptionLimits.compositeDepth) &&
1206
- counts.compositeCount + filter.compositeFilter.length > SubscriptionLimits.compositeCount) {
1207
- throw new ConnectError("SubscriptionService.Subscribe may contain at most 8 composite filters.", Code.InvalidArgument);
1208
- }
1194
+ }
1195
+ function validateSubscriptionFilterNode(filter, depth, counts) {
1196
+ if (depth > SubscriptionLimits.compositeDepth) {
1197
+ throw new ConnectError("SubscriptionService.Subscribe composite filters may nest at most 8 levels.", Code.InvalidArgument);
1209
1198
  }
1210
- function enqueueSubscriptionFilterChildren(filter, depth, stack) {
1211
- for (const nested of filter.compositeFilter) {
1212
- if (depth + 1 > SubscriptionLimits.compositeDepth) {
1213
- throw new ConnectError("SubscriptionService.Subscribe composite filters may nest at most 8 levels.", Code.InvalidArgument);
1214
- }
1215
- stack.push({ filter: nested, depth: depth + 1 });
1216
- }
1199
+ switch (filter.operator) {
1200
+ case CompositeFilter_CompositeOperator.ALL:
1201
+ case CompositeFilter_CompositeOperator.EITHER:
1202
+ break;
1203
+ default:
1204
+ throw new ConnectError("SubscriptionService.Subscribe supports only ALL or EITHER composite filters.", Code.InvalidArgument);
1217
1205
  }
1218
- function createCompositePredicate(filter, route) {
1219
- const children = [
1220
- ...filter.filter.map((simple) => createSimplePredicate(simple, route)),
1221
- ...filter.compositeFilter.map((nested) => createCompositePredicate(nested, route)),
1222
- ];
1223
- if (children.length === 0) {
1224
- return () => true;
1225
- }
1226
- return filter.operator === CompositeFilter_CompositeOperator.ALL
1227
- ? (state) => children.every((predicate) => predicate(state))
1228
- : (state) => children.some((predicate) => predicate(state));
1206
+ if (depth === SubscriptionLimits.compositeDepth && filter.compositeFilter.length > 0) {
1207
+ throw new ConnectError("SubscriptionService.Subscribe composite filters may nest at most 8 levels.", Code.InvalidArgument);
1229
1208
  }
1230
- function createSimplePredicate(filter, route) {
1231
- if (filter.operator !== Filter_Operator.EQUAL) {
1232
- throw new ConnectError("SubscriptionService.Subscribe supports only EQUAL field filters.", Code.InvalidArgument);
1233
- }
1234
- if (filter.value === undefined) {
1235
- throw new ConnectError("SubscriptionService.Subscribe field filter value is required.", Code.InvalidArgument);
1236
- }
1237
- validateSubscriptionPath(filter.fieldPath?.fieldName ?? [], "field filter");
1238
- const resolved = resolveMessagePath(route.schema, filter.fieldPath?.fieldName ?? [], "field filter");
1239
- const expected = decodeFieldFilterValue(filter.value, resolved.leafSchema);
1240
- return (state) => valuesEqual(readPathValue(state, resolved.localPath), expected, resolved.leafSchema);
1209
+ counts.compositeCount += 1;
1210
+ if (counts.compositeCount > SubscriptionLimits.compositeCount) {
1211
+ throw new ConnectError("SubscriptionService.Subscribe may contain at most 8 composite filters.", Code.InvalidArgument);
1241
1212
  }
1242
- function subscriptionFieldMask(topic, route) {
1243
- const paths = topic.fieldMask?.paths ?? [];
1244
- if (paths.length === 0) {
1245
- return undefined;
1246
- }
1247
- if (paths.length > SubscriptionLimits.maskPathCount) {
1248
- throw new ConnectError("SubscriptionService.Subscribe field_mask may contain at most 32 paths.", Code.InvalidArgument);
1249
- }
1250
- if (paths.some((path) => path.length > SubscriptionLimits.maskPathLength)) {
1251
- throw new ConnectError("SubscriptionService.Subscribe field_mask paths may contain at most 128 characters.", Code.InvalidArgument);
1252
- }
1253
- return paths.map((path) => {
1254
- const segments = path.split(".");
1255
- validateSubscriptionPath(segments, "field_mask");
1256
- return resolveMessagePath(route.schema, segments, "field_mask").localPath.join(".");
1257
- });
1213
+ counts.simpleCount += filter.filter.length;
1214
+ if (counts.simpleCount > SubscriptionLimits.filterCount) {
1215
+ throw new ConnectError("SubscriptionService.Subscribe may contain at most 16 simple field filters.", Code.InvalidArgument);
1258
1216
  }
1259
- function validateSubscriptionPath(fieldPath, label) {
1260
- if (fieldPath.length === 0 || fieldPath.some((field) => field.trim().length === 0)) {
1261
- throw new ConnectError(`SubscriptionService.Subscribe ${label} path is required.`, Code.InvalidArgument);
1262
- }
1263
- if (fieldPath.length > SubscriptionLimits.pathComponentCount) {
1264
- throw new ConnectError(`SubscriptionService.Subscribe ${label} path may contain at most 16 components.`, Code.InvalidArgument);
1265
- }
1266
- if (fieldPath.some((field) => field.length > SubscriptionLimits.pathSegmentLength)) {
1267
- throw new ConnectError(`SubscriptionService.Subscribe ${label} path components may contain at most 128 characters.`, Code.InvalidArgument);
1268
- }
1217
+ if ((filter.compositeFilter.length > 1 || depth + 1 < SubscriptionLimits.compositeDepth) &&
1218
+ counts.compositeCount + filter.compositeFilter.length > SubscriptionLimits.compositeCount) {
1219
+ throw new ConnectError("SubscriptionService.Subscribe may contain at most 8 composite filters.", Code.InvalidArgument);
1269
1220
  }
1270
- function decodeSubscriptionIdValue(value, route) {
1271
- if (!isAny(value)) {
1272
- throw new ConnectError("SubscriptionService.Subscribe id_filter values must be packed Any messages.", Code.InvalidArgument);
1273
- }
1274
- if (route.idField.message === undefined) {
1275
- return decodeAnyValue(value);
1276
- }
1277
- const decoded = AnyMessages.unpack(value, route.idField.message);
1278
- if (decoded === undefined) {
1279
- throw new ConnectError(`SubscriptionService.Subscribe id_filter values must pack ${route.idField.message.typeName}.`, Code.InvalidArgument);
1221
+ }
1222
+ function enqueueSubscriptionFilterChildren(filter, depth, stack) {
1223
+ for (const nested of filter.compositeFilter) {
1224
+ if (depth + 1 > SubscriptionLimits.compositeDepth) {
1225
+ throw new ConnectError("SubscriptionService.Subscribe composite filters may nest at most 8 levels.", Code.InvalidArgument);
1280
1226
  }
1281
- return decoded;
1227
+ stack.push({ filter: nested, depth: depth + 1 });
1282
1228
  }
1283
- function decodeFieldFilterValue(value, schema) {
1284
- if (value === undefined || schema === undefined) {
1285
- return decodeAnyValue(value);
1286
- }
1287
- const decoded = AnyMessages.unpack(value, schema);
1288
- if (decoded === undefined) {
1289
- throw new ConnectError(`SubscriptionService.Subscribe field filter value must pack ${schema.typeName}.`, Code.InvalidArgument);
1290
- }
1291
- return decoded;
1229
+ }
1230
+ function createCompositePredicate(filter, route) {
1231
+ const children = [
1232
+ ...filter.filter.map((simple) => createSimplePredicate(simple, route)),
1233
+ ...filter.compositeFilter.map((nested) => createCompositePredicate(nested, route)),
1234
+ ];
1235
+ if (children.length === 0) {
1236
+ return () => true;
1237
+ }
1238
+ return filter.operator === CompositeFilter_CompositeOperator.ALL
1239
+ ? (state) => children.every((predicate) => predicate(state))
1240
+ : (state) => children.some((predicate) => predicate(state));
1241
+ }
1242
+ function createSimplePredicate(filter, route) {
1243
+ if (filter.operator !== Filter_Operator.EQUAL) {
1244
+ throw new ConnectError("SubscriptionService.Subscribe supports only EQUAL field filters.", Code.InvalidArgument);
1292
1245
  }
1293
- function decodeAnyValue(value, schema) {
1294
- if (value === undefined) {
1295
- return undefined;
1296
- }
1297
- if (schema !== undefined) {
1298
- const decoded = AnyMessages.unpack(value, schema);
1299
- if (decoded !== undefined) {
1300
- return decoded;
1301
- }
1302
- }
1303
- for (const decoder of VALUE_DECODERS) {
1304
- const decoded = decoder(value);
1305
- if (decoded !== undefined) {
1306
- return decoded;
1307
- }
1308
- }
1309
- return value;
1246
+ if (filter.value === undefined) {
1247
+ throw new ConnectError("SubscriptionService.Subscribe field filter value is required.", Code.InvalidArgument);
1310
1248
  }
1311
- function resolveMessagePath(schema, fieldPath, label) {
1312
- const localPath = [];
1313
- let currentSchema = schema;
1314
- let leafSchema;
1315
- for (const [index, name] of fieldPath.entries()) {
1316
- const field = findMessageField(currentSchema, name);
1317
- if (field === undefined) {
1318
- throw new ConnectError(`SubscriptionService.Subscribe ${label} "${fieldPath.join(".")}" is not a state field.`, Code.InvalidArgument);
1319
- }
1320
- localPath.push(field.localName);
1321
- leafSchema = field.message;
1322
- currentSchema = index === fieldPath.length - 1 ? undefined : field.message;
1323
- if (currentSchema === undefined && index < fieldPath.length - 1) {
1324
- throw new ConnectError(`SubscriptionService.Subscribe ${label} "${fieldPath.join(".")}" is not a message path.`, Code.InvalidArgument);
1325
- }
1326
- }
1327
- return Object.freeze({
1328
- localPath,
1329
- ...(leafSchema === undefined ? {} : { leafSchema }),
1330
- });
1249
+ validateSubscriptionPath(filter.fieldPath?.fieldName ?? [], "field filter");
1250
+ const resolved = resolveMessagePath(route.schema, filter.fieldPath?.fieldName ?? [], "field filter");
1251
+ const expected = decodeFieldFilterValue(filter.value, resolved.leafSchema);
1252
+ return (state) => valuesEqual(readPathValue(state, resolved.localPath), expected, resolved.leafSchema);
1253
+ }
1254
+ /**
1255
+ * Checks a state filter path before resolving its fields.
1256
+ */
1257
+ function validateSubscriptionPath(fieldPath, label) {
1258
+ if (fieldPath.length === 0 || fieldPath.some((field) => field.trim().length === 0)) {
1259
+ throw new ConnectError(`SubscriptionService.Subscribe ${label} path is required.`, Code.InvalidArgument);
1331
1260
  }
1332
- function findMessageField(schema, name) {
1333
- const fields = (schema?.fields ?? []);
1334
- return fields.find((field) => field.name === name || field.localName === name);
1261
+ if (fieldPath.length > SubscriptionLimits.pathComponentCount) {
1262
+ throw new ConnectError(`SubscriptionService.Subscribe ${label} path may contain at most 16 components.`, Code.InvalidArgument);
1335
1263
  }
1336
- function readPathValue(value, path) {
1337
- let current = value;
1338
- for (const segment of path) {
1339
- if (typeof current !== "object" || current === null) {
1340
- return undefined;
1341
- }
1342
- current = Reflect.get(current, segment);
1343
- }
1344
- return current;
1264
+ if (fieldPath.some((field) => field.length > SubscriptionLimits.pathSegmentLength)) {
1265
+ throw new ConnectError(`SubscriptionService.Subscribe ${label} path components may contain at most 128 characters.`, Code.InvalidArgument);
1345
1266
  }
1346
- function valuesEqual(actual, expected, schema) {
1347
- if (schema !== undefined && isMessage(actual) && isMessage(expected)) {
1348
- return bytesEqual(toBinary(schema, actual), toBinary(schema, expected));
1349
- }
1350
- if (isAny(actual) && isAny(expected)) {
1351
- return bytesEqual(toBinary(AnySchema, actual), toBinary(AnySchema, expected));
1352
- }
1353
- if (actual instanceof Uint8Array && expected instanceof Uint8Array) {
1354
- return bytesEqual(actual, expected);
1355
- }
1356
- return Object.is(actual, expected);
1267
+ }
1268
+ function decodeSubscriptionIdValue(value, route) {
1269
+ if (!isAny(value)) {
1270
+ throw new ConnectError("SubscriptionService.Subscribe id_filter values must be packed Any messages.", Code.InvalidArgument);
1357
1271
  }
1358
- function isMessage(value) {
1359
- return typeof value === "object" && value !== null && "$typeName" in value;
1272
+ if (route.idField.message === undefined) {
1273
+ return decodeAnyValue(value);
1360
1274
  }
1361
- function isAny(value) {
1362
- return isMessage(value) && value.$typeName === "google.protobuf.Any";
1275
+ const decoded = AnyMessages.unpack(value, route.idField.message);
1276
+ if (decoded === undefined) {
1277
+ throw new ConnectError(`SubscriptionService.Subscribe id_filter values must pack ${route.idField.message.typeName}.`, Code.InvalidArgument);
1363
1278
  }
1364
- function bytesEqual(left, right) {
1365
- return left.length === right.length && left.every((byte, index) => byte === right[index]);
1279
+ return decoded;
1280
+ }
1281
+ function decodeFieldFilterValue(value, schema) {
1282
+ if (value === undefined || schema === undefined) {
1283
+ return decodeAnyValue(value);
1366
1284
  }
1367
- function packVersionedState(schema, result) {
1368
- return create(EntityStateVersionSchema, {
1369
- state: AnyMessages.pack(schema, result.state, { validate: false }),
1370
- version: result.version ?? create(VersionSchema),
1371
- });
1285
+ const decoded = AnyMessages.unpack(value, schema);
1286
+ if (decoded === undefined) {
1287
+ throw new ConnectError(`SubscriptionService.Subscribe field filter value must pack ${schema.typeName}.`, Code.InvalidArgument);
1372
1288
  }
1373
- function commandTenant(command) {
1374
- return tenantValue(command.context?.actorContext?.tenantId);
1289
+ return decoded;
1290
+ }
1291
+ function decodeAnyValue(value, schema) {
1292
+ if (value === undefined) {
1293
+ return undefined;
1375
1294
  }
1376
- function topicTenant(topic) {
1377
- return tenantValue(topic?.context?.tenantId);
1295
+ if (schema !== undefined) {
1296
+ const decoded = AnyMessages.unpack(value, schema);
1297
+ if (decoded !== undefined) {
1298
+ return decoded;
1299
+ }
1378
1300
  }
1379
- function eventTenant(event) {
1380
- switch (event.context?.origin.case) {
1381
- case "importContext":
1382
- return tenantValue(event.context.origin.value.tenantId);
1383
- case "pastMessage":
1384
- return tenantValue(event.context.origin.value.actorContext?.tenantId);
1385
- default:
1386
- return undefined;
1301
+ for (const decoder of VALUE_DECODERS) {
1302
+ const decoded = decoder(value);
1303
+ if (decoded !== undefined) {
1304
+ return decoded;
1387
1305
  }
1388
1306
  }
1389
- function eventTenantMatches(record, event) {
1390
- const tenantId = eventTenant(event);
1391
- return (record.tenantId === undefined ||
1392
- (tenantId !== undefined &&
1393
- TenantBoundary.from(tenantId).key === TenantBoundary.from(record.tenantId).key));
1307
+ return value;
1308
+ }
1309
+ /**
1310
+ * Resolves a state filter path to generated message fields.
1311
+ */
1312
+ function resolveMessagePath(schema, fieldPath, label) {
1313
+ const localPath = [];
1314
+ let currentSchema = schema;
1315
+ let leafSchema;
1316
+ for (const [index, name] of fieldPath.entries()) {
1317
+ const field = findMessageField(currentSchema, name);
1318
+ if (field === undefined) {
1319
+ throw new ConnectError(`SubscriptionService.Subscribe ${label} "${fieldPath.join(".")}" is not a state field.`, Code.InvalidArgument);
1320
+ }
1321
+ localPath.push(field.localName);
1322
+ leafSchema = field.message;
1323
+ currentSchema = index === fieldPath.length - 1 ? undefined : field.message;
1324
+ if (currentSchema === undefined && index < fieldPath.length - 1) {
1325
+ throw new ConnectError(`SubscriptionService.Subscribe ${label} "${fieldPath.join(".")}" is not a message path.`, Code.InvalidArgument);
1326
+ }
1394
1327
  }
1395
- function tenantValue(tenantId) {
1396
- if (tenantId === undefined)
1328
+ return Object.freeze({
1329
+ localPath,
1330
+ ...(leafSchema === undefined ? {} : { leafSchema }),
1331
+ });
1332
+ }
1333
+ function findMessageField(schema, name) {
1334
+ const fields = (schema?.fields ?? []);
1335
+ return fields.find((field) => field.name === name || field.localName === name);
1336
+ }
1337
+ function readPathValue(value, path) {
1338
+ let current = value;
1339
+ for (const segment of path) {
1340
+ if (typeof current !== "object" || current === null) {
1397
1341
  return undefined;
1398
- try {
1399
- return TenantBoundary.from(tenantId).tenantId;
1400
1342
  }
1401
- catch {
1343
+ current = Reflect.get(current, segment);
1344
+ }
1345
+ return current;
1346
+ }
1347
+ function valuesEqual(actual, expected, schema) {
1348
+ if (schema !== undefined && isMessage(actual) && isMessage(expected)) {
1349
+ return bytesEqual(toBinary(schema, actual), toBinary(schema, expected));
1350
+ }
1351
+ if (isAny(actual) && isAny(expected)) {
1352
+ return bytesEqual(toBinary(AnySchema, actual), toBinary(AnySchema, expected));
1353
+ }
1354
+ if (actual instanceof Uint8Array && expected instanceof Uint8Array) {
1355
+ return bytesEqual(actual, expected);
1356
+ }
1357
+ return Object.is(actual, expected);
1358
+ }
1359
+ function isMessage(value) {
1360
+ return typeof value === "object" && value !== null && "$typeName" in value;
1361
+ }
1362
+ function isAny(value) {
1363
+ return isMessage(value) && value.$typeName === "google.protobuf.Any";
1364
+ }
1365
+ function bytesEqual(left, right) {
1366
+ return left.length === right.length && left.every((byte, index) => byte === right[index]);
1367
+ }
1368
+ /**
1369
+ * Packs a state snapshot and version for a public query result.
1370
+ *
1371
+ * @typeParam Schema Generated state schema.
1372
+ * @param schema Schema used to pack the state.
1373
+ * @param result Stored state and its version.
1374
+ * @returns Wire state/version pair.
1375
+ */
1376
+ function packVersionedState(schema, result) {
1377
+ return create(EntityStateVersionSchema, {
1378
+ state: AnyMessages.pack(schema, result.state, { validate: false }),
1379
+ version: result.version ?? create(VersionSchema),
1380
+ });
1381
+ }
1382
+ function commandTenant(command) {
1383
+ return tenantValue(command.context?.actorContext?.tenantId);
1384
+ }
1385
+ function topicTenant(topic) {
1386
+ return tenantValue(topic?.context?.tenantId);
1387
+ }
1388
+ function eventTenant(event) {
1389
+ switch (event.context?.origin.case) {
1390
+ case "importContext":
1391
+ return tenantValue(event.context.origin.value.tenantId);
1392
+ case "pastMessage":
1393
+ return tenantValue(event.context.origin.value.actorContext?.tenantId);
1394
+ default:
1402
1395
  return undefined;
1403
- }
1404
1396
  }
1405
- function stateRouteIdField(schema, idField) {
1406
- const field = findMessageField(schema, idField?.name ?? "") ??
1407
- findMessageField(schema, idField?.localName ?? "") ??
1408
- schema.fields[0] ??
1409
- undefined;
1410
- if (field === undefined) {
1411
- throw new ConnectError("Subscription target ID field is not available.", Code.InvalidArgument);
1412
- }
1413
- return field;
1397
+ }
1398
+ function eventTenantMatches(record, event) {
1399
+ const tenantId = eventTenant(event);
1400
+ return (record.tenantId === undefined ||
1401
+ (tenantId !== undefined &&
1402
+ TenantBoundary.from(tenantId).key === TenantBoundary.from(record.tenantId).key));
1403
+ }
1404
+ function tenantValue(tenantId) {
1405
+ if (tenantId === undefined)
1406
+ return undefined;
1407
+ try {
1408
+ return TenantBoundary.from(tenantId).tenantId;
1414
1409
  }
1415
- function tenantMismatch(multitenant, tenantId, subject) {
1416
- if (multitenant) {
1417
- return tenantId === undefined
1418
- ? {
1419
- type: "TENANT_REQUIRED",
1420
- message: `Tenant is required for this ${subject}.`,
1421
- }
1422
- : undefined;
1423
- }
1410
+ catch {
1411
+ return undefined;
1412
+ }
1413
+ }
1414
+ function stateRouteIdField(schema, idField) {
1415
+ const field = findMessageField(schema, idField?.name ?? "") ??
1416
+ findMessageField(schema, idField?.localName ?? "") ??
1417
+ schema.fields[0] ??
1418
+ undefined;
1419
+ if (field === undefined) {
1420
+ throw new ConnectError("Subscription target ID field is not available.", Code.InvalidArgument);
1421
+ }
1422
+ return field;
1423
+ }
1424
+ function tenantMismatch(multitenant, tenantId, subject) {
1425
+ if (multitenant) {
1424
1426
  return tenantId === undefined
1425
- ? undefined
1426
- : {
1427
- type: "TENANT_INAPPLICABLE",
1428
- message: `Tenant is not applicable for this ${subject}.`,
1429
- };
1427
+ ? {
1428
+ type: "TENANT_REQUIRED",
1429
+ message: `Tenant is required for this ${subject}.`,
1430
+ }
1431
+ : undefined;
1430
1432
  }
1431
- function commandPostError(error) {
1432
- if (error instanceof TransitionValidationError) {
1433
- return {
1434
- type: error.type,
1435
- message: error.clientMessage,
1436
- details: AnyMessages.pack(ValidationErrorSchema, error.validationError, {
1437
- validate: false,
1438
- }),
1439
- };
1440
- }
1441
- if (error instanceof CommandValidationError) {
1442
- return {
1443
- type: "COMMAND_VALIDATION_ERROR",
1444
- message: "Command payload validation failed.",
1445
- details: AnyMessages.pack(ValidationErrorSchema, error.validationError, {
1446
- validate: false,
1447
- }),
1448
- };
1449
- }
1433
+ return tenantId === undefined
1434
+ ? undefined
1435
+ : {
1436
+ type: "TENANT_INAPPLICABLE",
1437
+ message: `Tenant is not applicable for this ${subject}.`,
1438
+ };
1439
+ }
1440
+ function commandPostError(error) {
1441
+ if (error instanceof TransitionValidationError) {
1450
1442
  return {
1451
- type: "COMMAND_POST_ERROR",
1452
- message: "Command post failed.",
1443
+ type: error.type,
1444
+ message: error.clientMessage,
1445
+ details: AnyMessages.pack(ValidationErrorSchema, error.validationError, {
1446
+ validate: false,
1447
+ }),
1453
1448
  };
1454
1449
  }
1455
- function tenantOptions(tenantId) {
1456
- return tenantId === undefined ? {} : { tenantId: clone(TenantIdSchema, tenantId) };
1457
- }
1458
- const VALUE_DECODERS = Object.freeze([
1459
- (value) => AnyMessages.unpack(value, StringValueSchema)?.value,
1460
- (value) => AnyMessages.unpack(value, BoolValueSchema)?.value,
1461
- (value) => AnyMessages.unpack(value, Int32ValueSchema)?.value,
1462
- (value) => AnyMessages.unpack(value, UInt32ValueSchema)?.value,
1463
- (value) => AnyMessages.unpack(value, Int64ValueSchema)?.value,
1464
- (value) => AnyMessages.unpack(value, UInt64ValueSchema)?.value,
1465
- (value) => AnyMessages.unpack(value, FloatValueSchema)?.value,
1466
- (value) => AnyMessages.unpack(value, DoubleValueSchema)?.value,
1467
- (value) => AnyMessages.unpack(value, BytesValueSchema)?.value,
1468
- ]);
1469
- function createEntityUpdate(record, update) {
1470
- const matcher = record.matcher;
1471
- if (matcher === undefined)
1472
- return undefined;
1473
- const match = matcher.match(update);
1474
- if (match === undefined) {
1475
- return undefined;
1476
- }
1477
- return create(SubscriptionUpdateSchema, {
1478
- subscription: clone(SubscriptionSchema, record.subscription),
1479
- response: okResponse(),
1480
- update: {
1481
- case: "entityUpdates",
1482
- value: create(EntityUpdatesSchema, {
1483
- update: [
1484
- create(EntityStateUpdateSchema, {
1485
- id: packEntityId(record.route, update.id),
1486
- kind: match === "state"
1487
- ? {
1488
- case: "state",
1489
- value: AnyMessages.pack(record.route.schema, maskedState(record, update.state), {
1490
- validate: false,
1491
- }),
1492
- }
1493
- : {
1494
- case: "noLongerMatching",
1495
- value: true,
1496
- },
1497
- }),
1498
- ],
1499
- }),
1500
- },
1501
- });
1450
+ if (error instanceof CommandValidationError) {
1451
+ return {
1452
+ type: "COMMAND_VALIDATION_ERROR",
1453
+ message: "Command payload validation failed.",
1454
+ details: AnyMessages.pack(ValidationErrorSchema, error.validationError, {
1455
+ validate: false,
1456
+ }),
1457
+ };
1502
1458
  }
1503
- function createEventUpdate(record, event) {
1504
- return create(SubscriptionUpdateSchema, {
1505
- subscription: clone(SubscriptionSchema, record.subscription),
1506
- response: okResponse(),
1507
- update: {
1508
- case: "eventUpdates",
1509
- value: create(EventUpdatesSchema, {
1510
- event: [cloneClientEvent(event)],
1511
- }),
1512
- },
1513
- });
1459
+ return {
1460
+ type: "COMMAND_POST_ERROR",
1461
+ message: "Command post failed.",
1462
+ };
1463
+ }
1464
+ function tenantOptions(tenantId) {
1465
+ return tenantId === undefined ? {} : { tenantId: clone(TenantIdSchema, tenantId) };
1466
+ }
1467
+ const VALUE_DECODERS = Object.freeze([
1468
+ (value) => AnyMessages.unpack(value, StringValueSchema)?.value,
1469
+ (value) => AnyMessages.unpack(value, BoolValueSchema)?.value,
1470
+ (value) => AnyMessages.unpack(value, Int32ValueSchema)?.value,
1471
+ (value) => AnyMessages.unpack(value, UInt32ValueSchema)?.value,
1472
+ (value) => AnyMessages.unpack(value, Int64ValueSchema)?.value,
1473
+ (value) => AnyMessages.unpack(value, UInt64ValueSchema)?.value,
1474
+ (value) => AnyMessages.unpack(value, FloatValueSchema)?.value,
1475
+ (value) => AnyMessages.unpack(value, DoubleValueSchema)?.value,
1476
+ (value) => AnyMessages.unpack(value, BytesValueSchema)?.value,
1477
+ ]);
1478
+ /**
1479
+ * Builds a subscription update with a complete state or a removal signal.
1480
+ */
1481
+ function createEntityUpdate(record, update) {
1482
+ const matcher = record.matcher;
1483
+ if (matcher === undefined)
1484
+ return undefined;
1485
+ const match = matcher.match(update);
1486
+ if (match === undefined)
1487
+ return undefined;
1488
+ const kind = match === "state"
1489
+ ? {
1490
+ case: "state",
1491
+ value: AnyMessages.pack(record.route.schema, clone(record.route.schema, update.state), {
1492
+ validate: false,
1493
+ }),
1494
+ }
1495
+ : { case: "noLongerMatching", value: true };
1496
+ return create(SubscriptionUpdateSchema, {
1497
+ subscription: clone(SubscriptionSchema, record.subscription),
1498
+ response: okResponse(),
1499
+ update: {
1500
+ case: "entityUpdates",
1501
+ value: create(EntityUpdatesSchema, {
1502
+ update: [
1503
+ create(EntityStateUpdateSchema, {
1504
+ id: packEntityId(record.route, update.id),
1505
+ kind,
1506
+ }),
1507
+ ],
1508
+ }),
1509
+ },
1510
+ });
1511
+ }
1512
+ function createEventUpdate(record, event) {
1513
+ return create(SubscriptionUpdateSchema, {
1514
+ subscription: clone(SubscriptionSchema, record.subscription),
1515
+ response: okResponse(),
1516
+ update: {
1517
+ case: "eventUpdates",
1518
+ value: create(EventUpdatesSchema, {
1519
+ event: [cloneClientEvent(event)],
1520
+ }),
1521
+ },
1522
+ });
1523
+ }
1524
+ function cloneClientEvent(event) {
1525
+ const clientEvent = clone(EventSchema, event);
1526
+ const rejection = clientEvent.context?.rejection;
1527
+ if (rejection !== undefined) {
1528
+ rejection.command = undefined;
1529
+ // Clear the legacy wire payload as part of client-side security redaction.
1530
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
1531
+ rejection.commandMessage = undefined;
1532
+ rejection.stacktrace = "";
1533
+ }
1534
+ return clientEvent;
1535
+ }
1536
+ function packString(value) {
1537
+ return AnyMessages.pack(StringValueSchema, create(StringValueSchema, { value }));
1538
+ }
1539
+ function packEntityId(route, id) {
1540
+ if (isAny(id)) {
1541
+ return clone(AnySchema, id);
1542
+ }
1543
+ if (route.idField.message !== undefined && isMessage(id)) {
1544
+ return AnyMessages.pack(route.idField.message, id, { validate: false });
1545
+ }
1546
+ if (id instanceof Uint8Array) {
1547
+ return AnyMessages.pack(BytesValueSchema, create(BytesValueSchema, { value: id }));
1548
+ }
1549
+ switch (typeof id) {
1550
+ case "string":
1551
+ return packString(id);
1552
+ case "boolean":
1553
+ return AnyMessages.pack(BoolValueSchema, create(BoolValueSchema, { value: id }));
1554
+ case "number":
1555
+ return AnyMessages.pack(DoubleValueSchema, create(DoubleValueSchema, { value: id }));
1556
+ case "bigint":
1557
+ return AnyMessages.pack(Int64ValueSchema, create(Int64ValueSchema, { value: id }));
1558
+ default:
1559
+ return undefined;
1514
1560
  }
1515
- function cloneClientEvent(event) {
1516
- const clientEvent = clone(EventSchema, event);
1517
- const rejection = clientEvent.context?.rejection;
1518
- if (rejection !== undefined) {
1519
- rejection.command = undefined;
1520
- // Clear the legacy wire payload as part of client-side security redaction.
1521
- // eslint-disable-next-line @typescript-eslint/no-deprecated
1522
- rejection.commandMessage = undefined;
1523
- rejection.stacktrace = "";
1524
- }
1525
- return clientEvent;
1526
- }
1527
- function maskedState(record, state) {
1528
- return RecordMask.apply(clone(record.route.schema, state), record.matcher?.fieldMask);
1529
- }
1530
- function packString(value) {
1531
- return AnyMessages.pack(StringValueSchema, create(StringValueSchema, { value }));
1532
- }
1533
- function packEntityId(route, id) {
1534
- if (isAny(id)) {
1535
- return clone(AnySchema, id);
1536
- }
1537
- if (route.idField.message !== undefined && isMessage(id)) {
1538
- return AnyMessages.pack(route.idField.message, id, { validate: false });
1539
- }
1540
- if (id instanceof Uint8Array) {
1541
- return AnyMessages.pack(BytesValueSchema, create(BytesValueSchema, { value: id }));
1542
- }
1543
- switch (typeof id) {
1544
- case "string":
1545
- return packString(id);
1546
- case "boolean":
1547
- return AnyMessages.pack(BoolValueSchema, create(BoolValueSchema, { value: id }));
1548
- case "number":
1549
- return AnyMessages.pack(DoubleValueSchema, create(DoubleValueSchema, { value: id }));
1550
- case "bigint":
1551
- return AnyMessages.pack(Int64ValueSchema, create(Int64ValueSchema, { value: id }));
1552
- default:
1553
- return undefined;
1561
+ }
1562
+ /**
1563
+ * Groups non-public service contract, query, and subscription operations.
1564
+ */
1565
+ const ServiceValues = Object.freeze({
1566
+ /**
1567
+ * Checks one composite query filter and its current traversal bounds.
1568
+ *
1569
+ * @param filter The composite filter being traversed.
1570
+ * @param depth The nesting level of this filter.
1571
+ * @param compositeCount The number of composites visited so far.
1572
+ * @param simpleCount The number of simple filters visited so far.
1573
+ * @param pendingCount The number of composites waiting to be checked.
1574
+ * @param route The registered state type and columns used to validate filters.
1575
+ * @returns The first invalid-query error, if the filter is invalid.
1576
+ */
1577
+ validateCompositeNode(filter, depth, compositeCount, simpleCount, pendingCount, route) {
1578
+ if (filter.filter.length === 0 && filter.compositeFilter.length === 0)
1579
+ return invalidQueryError("QueryService.Read composite filter must not be empty.");
1580
+ if (compositeCount > QueryLimits.compositeCount || depth > 8)
1581
+ return invalidQueryError("QueryService.Read may contain at most 8 composite filters.");
1582
+ if (simpleCount > QueryLimits.filterCount)
1583
+ return invalidQueryError("QueryService.Read may contain at most 16 simple column filters.");
1584
+ if (compositeCount + pendingCount + filter.compositeFilter.length > QueryLimits.compositeCount)
1585
+ return invalidQueryError("QueryService.Read may contain at most 8 composite filters.");
1586
+ if (depth >= 8 && filter.compositeFilter.length > 0)
1587
+ return invalidQueryError("QueryService.Read may contain at most 8 composite filters.");
1588
+ if (filter.operator !== CompositeFilter_CompositeOperator.ALL &&
1589
+ filter.operator !== CompositeFilter_CompositeOperator.EITHER)
1590
+ return unsupportedFilterError("QueryService.Read composite operator must be ALL or EITHER.");
1591
+ return validateSimpleFilters(filter.filter, route);
1592
+ },
1593
+ /**
1594
+ * Checks that every wire ordering names a sortable registered column.
1595
+ *
1596
+ * @param orders The wire ordering clauses in request order.
1597
+ * @param route The registered columns available to this state type.
1598
+ * @returns The first unsupported-format error, if an ordering is invalid.
1599
+ */
1600
+ validateReadOrder(orders, route) {
1601
+ for (const order of orders) {
1602
+ if (order.column.trim().length === 0) {
1603
+ return unsupportedFormatError("QueryService.Read order_by column is required.");
1604
+ }
1605
+ if (!route.allowedColumnNames.has(order.column)) {
1606
+ return unsupportedFormatError(`QueryService.Read order_by column "${order.column}" is not a declared column.`);
1607
+ }
1608
+ const field = route.columnFields.get(order.column);
1609
+ if (field === undefined ? order.column !== "version" : !supportsRange(field)) {
1610
+ return unsupportedFormatError(`QueryService.Read order_by column "${order.column}" is not orderable.`);
1611
+ }
1612
+ if (order.direction !== OrderBy_Direction.ASCENDING &&
1613
+ order.direction !== OrderBy_Direction.DESCENDING) {
1614
+ return unsupportedFormatError("QueryService.Read order_by direction must be ASCENDING or DESCENDING.");
1615
+ }
1554
1616
  }
1555
- }
1556
- return Object.freeze({
1557
- commandPostError,
1558
- commandTenant,
1559
- createEntityUpdate,
1560
- createEventUpdate,
1561
- createReadPlan,
1562
- createSubscriptionRecord,
1563
- createSubscriptionRemoval,
1564
- createSubscriptionShape,
1565
- defaultQueueLimit: DEFAULT_QUEUE_LIMIT,
1566
- defaultSubscriptionLimit: DEFAULT_SUBSCRIPTION_LIMIT,
1567
- errorStatus,
1568
- eventTenantMatches,
1569
- okResponse,
1570
- okStatus,
1571
- positiveInteger,
1572
- packVersionedState,
1573
- queryErrorResponse,
1574
- queryResultLimit: QueryLimits.resultCount,
1575
- stateRouteIdField,
1576
- subscriptionLimit,
1577
- tenantMismatch,
1578
- tenantOptions,
1579
- tenantValue,
1580
- topicTenant,
1581
- uniqueContexts,
1582
- validateReadQuery,
1583
- validateTopic,
1584
- });
1585
- })();
1617
+ return undefined;
1618
+ },
1619
+ /**
1620
+ * Checks that a state topic has a usable ID or field filter.
1621
+ *
1622
+ * @param filters The ID and field criteria from the subscription topic.
1623
+ */
1624
+ validateSubscriptionFilters(filters) {
1625
+ if (filters.idFilter?.id.length === 0) {
1626
+ throw new ConnectError("SubscriptionService.Subscribe id_filter requires at least one ID.", Code.InvalidArgument);
1627
+ }
1628
+ if (filters.idFilter !== undefined && filters.idFilter.id.length > SubscriptionLimits.idCount) {
1629
+ throw new ConnectError("SubscriptionService.Subscribe id_filter may contain at most 100 IDs.", Code.InvalidArgument);
1630
+ }
1631
+ if (filters.idFilter === undefined && filters.filter.length === 0) {
1632
+ throw new ConnectError("SubscriptionService.Subscribe requires an ID filter or field filter.", Code.InvalidArgument);
1633
+ }
1634
+ validateSubscriptionFilterTree(filters.filter);
1635
+ },
1636
+ commandPostError,
1637
+ commandTenant,
1638
+ createEntityUpdate,
1639
+ createEventUpdate,
1640
+ createReadPlan,
1641
+ createSubscriptionRecord,
1642
+ createSubscriptionRemoval,
1643
+ createSubscriptionShape,
1644
+ defaultQueueLimit: DEFAULT_QUEUE_LIMIT,
1645
+ defaultSubscriptionLimit: DEFAULT_SUBSCRIPTION_LIMIT,
1646
+ errorStatus,
1647
+ eventTenantMatches,
1648
+ okResponse,
1649
+ okStatus,
1650
+ positiveInteger,
1651
+ packVersionedState,
1652
+ queryErrorResponse,
1653
+ queryResultLimit: QueryLimits.resultCount,
1654
+ stateRouteIdField,
1655
+ subscriptionLimit,
1656
+ tenantMismatch,
1657
+ tenantOptions,
1658
+ tenantValue,
1659
+ topicTenant,
1660
+ uniqueContexts,
1661
+ validateReadQuery,
1662
+ validateTopic,
1663
+ });
1586
1664
  //# sourceMappingURL=spine-services.js.map