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

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 (354) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +569 -0
  3. package/REFERENCE.md +488 -0
  4. package/dist/bus/command-bus.d.ts +61 -0
  5. package/dist/bus/command-bus.d.ts.map +1 -0
  6. package/dist/bus/command-bus.js +201 -0
  7. package/dist/bus/command-bus.js.map +1 -0
  8. package/dist/bus/command-dispatcher-registry.d.ts +33 -0
  9. package/dist/bus/command-dispatcher-registry.d.ts.map +1 -0
  10. package/dist/bus/command-dispatcher-registry.js +75 -0
  11. package/dist/bus/command-dispatcher-registry.js.map +1 -0
  12. package/dist/bus/command-dispatcher.d.ts +25 -0
  13. package/dist/bus/command-dispatcher.d.ts.map +1 -0
  14. package/dist/bus/command-dispatcher.js +15 -0
  15. package/dist/bus/command-dispatcher.js.map +1 -0
  16. package/dist/bus/command-errors.d.ts +23 -0
  17. package/dist/bus/command-errors.d.ts.map +1 -0
  18. package/dist/bus/command-errors.js +52 -0
  19. package/dist/bus/command-errors.js.map +1 -0
  20. package/dist/bus/event-bus.d.ts +113 -0
  21. package/dist/bus/event-bus.d.ts.map +1 -0
  22. package/dist/bus/event-bus.js +518 -0
  23. package/dist/bus/event-bus.js.map +1 -0
  24. package/dist/bus/event-dispatcher-origin-schemas.d.ts +17 -0
  25. package/dist/bus/event-dispatcher-origin-schemas.d.ts.map +1 -0
  26. package/dist/bus/event-dispatcher-origin-schemas.js +32 -0
  27. package/dist/bus/event-dispatcher-origin-schemas.js.map +1 -0
  28. package/dist/bus/event-dispatcher-registry.d.ts +48 -0
  29. package/dist/bus/event-dispatcher-registry.d.ts.map +1 -0
  30. package/dist/bus/event-dispatcher-registry.js +176 -0
  31. package/dist/bus/event-dispatcher-registry.js.map +1 -0
  32. package/dist/bus/event-dispatcher.d.ts +48 -0
  33. package/dist/bus/event-dispatcher.d.ts.map +1 -0
  34. package/dist/bus/event-dispatcher.js +15 -0
  35. package/dist/bus/event-dispatcher.js.map +1 -0
  36. package/dist/context/bounded-context.d.ts +606 -0
  37. package/dist/context/bounded-context.d.ts.map +1 -0
  38. package/dist/context/bounded-context.js +1780 -0
  39. package/dist/context/bounded-context.js.map +1 -0
  40. package/dist/context/entity-inbox.d.ts +63 -0
  41. package/dist/context/entity-inbox.d.ts.map +1 -0
  42. package/dist/context/entity-inbox.js +380 -0
  43. package/dist/context/entity-inbox.js.map +1 -0
  44. package/dist/context/local-inbox-handoff.d.ts +244 -0
  45. package/dist/context/local-inbox-handoff.d.ts.map +1 -0
  46. package/dist/context/local-inbox-handoff.js +334 -0
  47. package/dist/context/local-inbox-handoff.js.map +1 -0
  48. package/dist/context/projection-handoff.d.ts +46 -0
  49. package/dist/context/projection-handoff.d.ts.map +1 -0
  50. package/dist/context/projection-handoff.js +172 -0
  51. package/dist/context/projection-handoff.js.map +1 -0
  52. package/dist/context/tenant-index.d.ts +41 -0
  53. package/dist/context/tenant-index.d.ts.map +1 -0
  54. package/dist/context/tenant-index.js +92 -0
  55. package/dist/context/tenant-index.js.map +1 -0
  56. package/dist/delivery/conditional-pickup.d.ts +25 -0
  57. package/dist/delivery/conditional-pickup.d.ts.map +1 -0
  58. package/dist/delivery/conditional-pickup.js +26 -0
  59. package/dist/delivery/conditional-pickup.js.map +1 -0
  60. package/dist/delivery/delivery-builder.d.ts +216 -0
  61. package/dist/delivery/delivery-builder.d.ts.map +1 -0
  62. package/dist/delivery/delivery-builder.js +323 -0
  63. package/dist/delivery/delivery-builder.js.map +1 -0
  64. package/dist/delivery/delivery-lease.d.ts +15 -0
  65. package/dist/delivery/delivery-lease.d.ts.map +1 -0
  66. package/dist/delivery/delivery-lease.js +44 -0
  67. package/dist/delivery/delivery-lease.js.map +1 -0
  68. package/dist/delivery/delivery-loop.d.ts +113 -0
  69. package/dist/delivery/delivery-loop.d.ts.map +1 -0
  70. package/dist/delivery/delivery-loop.js +119 -0
  71. package/dist/delivery/delivery-loop.js.map +1 -0
  72. package/dist/delivery/delivery-monitor.d.ts +165 -0
  73. package/dist/delivery/delivery-monitor.d.ts.map +1 -0
  74. package/dist/delivery/delivery-monitor.js +168 -0
  75. package/dist/delivery/delivery-monitor.js.map +1 -0
  76. package/dist/delivery/delivery-ports.d.ts +141 -0
  77. package/dist/delivery/delivery-ports.d.ts.map +1 -0
  78. package/dist/delivery/delivery-ports.js +15 -0
  79. package/dist/delivery/delivery-ports.js.map +1 -0
  80. package/dist/delivery/delivery-run-control.d.ts +50 -0
  81. package/dist/delivery/delivery-run-control.d.ts.map +1 -0
  82. package/dist/delivery/delivery-run-control.js +66 -0
  83. package/dist/delivery/delivery-run-control.js.map +1 -0
  84. package/dist/delivery/delivery-run-coordinator.d.ts +218 -0
  85. package/dist/delivery/delivery-run-coordinator.d.ts.map +1 -0
  86. package/dist/delivery/delivery-run-coordinator.js +609 -0
  87. package/dist/delivery/delivery-run-coordinator.js.map +1 -0
  88. package/dist/delivery/delivery-storage-error.d.ts +13 -0
  89. package/dist/delivery/delivery-storage-error.d.ts.map +1 -0
  90. package/dist/delivery/delivery-storage-error.js +30 -0
  91. package/dist/delivery/delivery-storage-error.js.map +1 -0
  92. package/dist/delivery/delivery-supervisor.d.ts +175 -0
  93. package/dist/delivery/delivery-supervisor.d.ts.map +1 -0
  94. package/dist/delivery/delivery-supervisor.js +469 -0
  95. package/dist/delivery/delivery-supervisor.js.map +1 -0
  96. package/dist/delivery/delivery-worker.d.ts +180 -0
  97. package/dist/delivery/delivery-worker.d.ts.map +1 -0
  98. package/dist/delivery/delivery-worker.js +232 -0
  99. package/dist/delivery/delivery-worker.js.map +1 -0
  100. package/dist/delivery/delivery.d.ts +238 -0
  101. package/dist/delivery/delivery.d.ts.map +1 -0
  102. package/dist/delivery/delivery.js +400 -0
  103. package/dist/delivery/delivery.js.map +1 -0
  104. package/dist/delivery/inbox-records.d.ts +19 -0
  105. package/dist/delivery/inbox-records.d.ts.map +1 -0
  106. package/dist/delivery/inbox-records.js +291 -0
  107. package/dist/delivery/inbox-records.js.map +1 -0
  108. package/dist/delivery/inbox-storage.d.ts +81 -0
  109. package/dist/delivery/inbox-storage.d.ts.map +1 -0
  110. package/dist/delivery/inbox-storage.js +353 -0
  111. package/dist/delivery/inbox-storage.js.map +1 -0
  112. package/dist/delivery/inbox.d.ts +263 -0
  113. package/dist/delivery/inbox.d.ts.map +1 -0
  114. package/dist/delivery/inbox.js +172 -0
  115. package/dist/delivery/inbox.js.map +1 -0
  116. package/dist/delivery/parked-delivery-obligations.d.ts +182 -0
  117. package/dist/delivery/parked-delivery-obligations.d.ts.map +1 -0
  118. package/dist/delivery/parked-delivery-obligations.js +390 -0
  119. package/dist/delivery/parked-delivery-obligations.js.map +1 -0
  120. package/dist/delivery/shard-index.d.ts +41 -0
  121. package/dist/delivery/shard-index.d.ts.map +1 -0
  122. package/dist/delivery/shard-index.js +67 -0
  123. package/dist/delivery/shard-index.js.map +1 -0
  124. package/dist/delivery/sharded-work-registry.d.ts +114 -0
  125. package/dist/delivery/sharded-work-registry.d.ts.map +1 -0
  126. package/dist/delivery/sharded-work-registry.js +360 -0
  127. package/dist/delivery/sharded-work-registry.js.map +1 -0
  128. package/dist/entity/entity-metadata.d.ts +173 -0
  129. package/dist/entity/entity-metadata.d.ts.map +1 -0
  130. package/dist/entity/entity-metadata.js +187 -0
  131. package/dist/entity/entity-metadata.js.map +1 -0
  132. package/dist/entity/entity-storage-descriptor.d.ts +64 -0
  133. package/dist/entity/entity-storage-descriptor.d.ts.map +1 -0
  134. package/dist/entity/entity-storage-descriptor.js +227 -0
  135. package/dist/entity/entity-storage-descriptor.js.map +1 -0
  136. package/dist/entity/entity-transaction.d.ts +372 -0
  137. package/dist/entity/entity-transaction.d.ts.map +1 -0
  138. package/dist/entity/entity-transaction.js +413 -0
  139. package/dist/entity/entity-transaction.js.map +1 -0
  140. package/dist/entity/entity-transition-validation.d.ts +47 -0
  141. package/dist/entity/entity-transition-validation.d.ts.map +1 -0
  142. package/dist/entity/entity-transition-validation.js +327 -0
  143. package/dist/entity/entity-transition-validation.js.map +1 -0
  144. package/dist/entity/entity.d.ts +499 -0
  145. package/dist/entity/entity.d.ts.map +1 -0
  146. package/dist/entity/entity.js +829 -0
  147. package/dist/entity/entity.js.map +1 -0
  148. package/dist/entity/implicit-required-id.d.ts +15 -0
  149. package/dist/entity/implicit-required-id.d.ts.map +1 -0
  150. package/dist/entity/implicit-required-id.js +123 -0
  151. package/dist/entity/implicit-required-id.js.map +1 -0
  152. package/dist/entity/spec-scanner.d.ts +14 -0
  153. package/dist/entity/spec-scanner.d.ts.map +1 -0
  154. package/dist/entity/spec-scanner.js +37 -0
  155. package/dist/entity/spec-scanner.js.map +1 -0
  156. package/dist/handler/build-time-handler-analyzer.d.ts +150 -0
  157. package/dist/handler/build-time-handler-analyzer.d.ts.map +1 -0
  158. package/dist/handler/build-time-handler-analyzer.js +1101 -0
  159. package/dist/handler/build-time-handler-analyzer.js.map +1 -0
  160. package/dist/handler/command-registration-readiness.d.ts +99 -0
  161. package/dist/handler/command-registration-readiness.d.ts.map +1 -0
  162. package/dist/handler/command-registration-readiness.js +118 -0
  163. package/dist/handler/command-registration-readiness.js.map +1 -0
  164. package/dist/handler/event-handler-filter.d.ts +43 -0
  165. package/dist/handler/event-handler-filter.d.ts.map +1 -0
  166. package/dist/handler/event-handler-filter.js +128 -0
  167. package/dist/handler/event-handler-filter.js.map +1 -0
  168. package/dist/handler/event-registration-readiness.d.ts +196 -0
  169. package/dist/handler/event-registration-readiness.d.ts.map +1 -0
  170. package/dist/handler/event-registration-readiness.js +187 -0
  171. package/dist/handler/event-registration-readiness.js.map +1 -0
  172. package/dist/handler/external.d.ts +10 -0
  173. package/dist/handler/external.d.ts.map +1 -0
  174. package/dist/handler/external.js +15 -0
  175. package/dist/handler/external.js.map +1 -0
  176. package/dist/handler/generated-handler-registry.d.ts +154 -0
  177. package/dist/handler/generated-handler-registry.d.ts.map +1 -0
  178. package/dist/handler/generated-handler-registry.js +223 -0
  179. package/dist/handler/generated-handler-registry.js.map +1 -0
  180. package/dist/handler/generated-registry-discovery.d.ts +94 -0
  181. package/dist/handler/generated-registry-discovery.d.ts.map +1 -0
  182. package/dist/handler/generated-registry-discovery.js +228 -0
  183. package/dist/handler/generated-registry-discovery.js.map +1 -0
  184. package/dist/handler/generated-registry-writer.d.ts +58 -0
  185. package/dist/handler/generated-registry-writer.d.ts.map +1 -0
  186. package/dist/handler/generated-registry-writer.js +458 -0
  187. package/dist/handler/generated-registry-writer.js.map +1 -0
  188. package/dist/handler/handler-codegen.d.ts +48 -0
  189. package/dist/handler/handler-codegen.d.ts.map +1 -0
  190. package/dist/handler/handler-codegen.js +214 -0
  191. package/dist/handler/handler-codegen.js.map +1 -0
  192. package/dist/handler/handler-decorators.d.ts +134 -0
  193. package/dist/handler/handler-decorators.d.ts.map +1 -0
  194. package/dist/handler/handler-decorators.js +239 -0
  195. package/dist/handler/handler-decorators.js.map +1 -0
  196. package/dist/handler/handler-metadata.d.ts +510 -0
  197. package/dist/handler/handler-metadata.d.ts.map +1 -0
  198. package/dist/handler/handler-metadata.js +391 -0
  199. package/dist/handler/handler-metadata.js.map +1 -0
  200. package/dist/handler/registration-readiness-metadata.d.ts +68 -0
  201. package/dist/handler/registration-readiness-metadata.d.ts.map +1 -0
  202. package/dist/handler/registration-readiness-metadata.js +170 -0
  203. package/dist/handler/registration-readiness-metadata.js.map +1 -0
  204. package/dist/handler/rejection-source.d.ts +11 -0
  205. package/dist/handler/rejection-source.d.ts.map +1 -0
  206. package/dist/handler/rejection-source.js +25 -0
  207. package/dist/handler/rejection-source.js.map +1 -0
  208. package/dist/index.d.ts +46 -0
  209. package/dist/index.d.ts.map +1 -0
  210. package/dist/index.js +54 -0
  211. package/dist/index.js.map +1 -0
  212. package/dist/integration/external-messages.d.ts +49 -0
  213. package/dist/integration/external-messages.d.ts.map +1 -0
  214. package/dist/integration/external-messages.js +112 -0
  215. package/dist/integration/external-messages.js.map +1 -0
  216. package/dist/integration/integration-broker.d.ts +74 -0
  217. package/dist/integration/integration-broker.d.ts.map +1 -0
  218. package/dist/integration/integration-broker.js +444 -0
  219. package/dist/integration/integration-broker.js.map +1 -0
  220. package/dist/integration/third-party-context.d.ts +56 -0
  221. package/dist/integration/third-party-context.d.ts.map +1 -0
  222. package/dist/integration/third-party-context.js +130 -0
  223. package/dist/integration/third-party-context.js.map +1 -0
  224. package/dist/repository/command-errors.d.ts +25 -0
  225. package/dist/repository/command-errors.d.ts.map +1 -0
  226. package/dist/repository/command-errors.js +43 -0
  227. package/dist/repository/command-errors.js.map +1 -0
  228. package/dist/repository/command-routing.d.ts +72 -0
  229. package/dist/repository/command-routing.d.ts.map +1 -0
  230. package/dist/repository/command-routing.js +98 -0
  231. package/dist/repository/command-routing.js.map +1 -0
  232. package/dist/repository/commit-fence.d.ts +22 -0
  233. package/dist/repository/commit-fence.d.ts.map +1 -0
  234. package/dist/repository/commit-fence.js +37 -0
  235. package/dist/repository/commit-fence.js.map +1 -0
  236. package/dist/repository/event-routing.d.ts +71 -0
  237. package/dist/repository/event-routing.d.ts.map +1 -0
  238. package/dist/repository/event-routing.js +96 -0
  239. package/dist/repository/event-routing.js.map +1 -0
  240. package/dist/repository/primitive-id.d.ts +86 -0
  241. package/dist/repository/primitive-id.d.ts.map +1 -0
  242. package/dist/repository/primitive-id.js +110 -0
  243. package/dist/repository/primitive-id.js.map +1 -0
  244. package/dist/repository/repository.d.ts +670 -0
  245. package/dist/repository/repository.d.ts.map +1 -0
  246. package/dist/repository/repository.js +3298 -0
  247. package/dist/repository/repository.js.map +1 -0
  248. package/dist/repository/routing-declarations.d.ts +75 -0
  249. package/dist/repository/routing-declarations.d.ts.map +1 -0
  250. package/dist/repository/routing-declarations.js +101 -0
  251. package/dist/repository/routing-declarations.js.map +1 -0
  252. package/dist/repository/state-update-routing.d.ts +66 -0
  253. package/dist/repository/state-update-routing.d.ts.map +1 -0
  254. package/dist/repository/state-update-routing.js +91 -0
  255. package/dist/repository/state-update-routing.js.map +1 -0
  256. package/dist/runtime/runtime.d.ts +156 -0
  257. package/dist/runtime/runtime.d.ts.map +1 -0
  258. package/dist/runtime/runtime.js +215 -0
  259. package/dist/runtime/runtime.js.map +1 -0
  260. package/dist/runtime/signal-intake.d.ts +84 -0
  261. package/dist/runtime/signal-intake.d.ts.map +1 -0
  262. package/dist/runtime/signal-intake.js +94 -0
  263. package/dist/runtime/signal-intake.js.map +1 -0
  264. package/dist/runtime/signal-metadata.d.ts +242 -0
  265. package/dist/runtime/signal-metadata.d.ts.map +1 -0
  266. package/dist/runtime/signal-metadata.js +368 -0
  267. package/dist/runtime/signal-metadata.js.map +1 -0
  268. package/dist/server/browser-server.d.ts +49 -0
  269. package/dist/server/browser-server.d.ts.map +1 -0
  270. package/dist/server/browser-server.js +633 -0
  271. package/dist/server/browser-server.js.map +1 -0
  272. package/dist/server/durable-subscription-bindings.d.ts +122 -0
  273. package/dist/server/durable-subscription-bindings.d.ts.map +1 -0
  274. package/dist/server/durable-subscription-bindings.js +524 -0
  275. package/dist/server/durable-subscription-bindings.js.map +1 -0
  276. package/dist/server/environment-attachment.d.ts +356 -0
  277. package/dist/server/environment-attachment.d.ts.map +1 -0
  278. package/dist/server/environment-attachment.js +2061 -0
  279. package/dist/server/environment-attachment.js.map +1 -0
  280. package/dist/server/environment-delivery-records.d.ts +77 -0
  281. package/dist/server/environment-delivery-records.d.ts.map +1 -0
  282. package/dist/server/environment-delivery-records.js +282 -0
  283. package/dist/server/environment-delivery-records.js.map +1 -0
  284. package/dist/server/environment-delivery-worker.d.ts +161 -0
  285. package/dist/server/environment-delivery-worker.d.ts.map +1 -0
  286. package/dist/server/environment-delivery-worker.js +664 -0
  287. package/dist/server/environment-delivery-worker.js.map +1 -0
  288. package/dist/server/environment.d.ts +43 -0
  289. package/dist/server/environment.d.ts.map +1 -0
  290. package/dist/server/environment.js +90 -0
  291. package/dist/server/environment.js.map +1 -0
  292. package/dist/server/managed-child-subscription.d.ts +11 -0
  293. package/dist/server/managed-child-subscription.d.ts.map +1 -0
  294. package/dist/server/managed-child-subscription.js +44 -0
  295. package/dist/server/managed-child-subscription.js.map +1 -0
  296. package/dist/server/managed-server-application.d.ts +324 -0
  297. package/dist/server/managed-server-application.d.ts.map +1 -0
  298. package/dist/server/managed-server-application.js +937 -0
  299. package/dist/server/managed-server-application.js.map +1 -0
  300. package/dist/server/node-coordinator.d.ts +191 -0
  301. package/dist/server/node-coordinator.d.ts.map +1 -0
  302. package/dist/server/node-coordinator.js +552 -0
  303. package/dist/server/node-coordinator.js.map +1 -0
  304. package/dist/server/process-server-coordinator.d.ts +15 -0
  305. package/dist/server/process-server-coordinator.d.ts.map +1 -0
  306. package/dist/server/process-server-coordinator.js +112 -0
  307. package/dist/server/process-server-coordinator.js.map +1 -0
  308. package/dist/server/reserved-spine-rpc-paths.json +8 -0
  309. package/dist/server/retryable-close.d.ts +28 -0
  310. package/dist/server/retryable-close.d.ts.map +1 -0
  311. package/dist/server/retryable-close.js +129 -0
  312. package/dist/server/retryable-close.js.map +1 -0
  313. package/dist/server/server-environment.d.ts +180 -0
  314. package/dist/server/server-environment.d.ts.map +1 -0
  315. package/dist/server/server-environment.js +418 -0
  316. package/dist/server/server-environment.js.map +1 -0
  317. package/dist/server/server-log.d.ts +18 -0
  318. package/dist/server/server-log.d.ts.map +1 -0
  319. package/dist/server/server-log.js +96 -0
  320. package/dist/server/server-log.js.map +1 -0
  321. package/dist/server/server.d.ts +389 -0
  322. package/dist/server/server.d.ts.map +1 -0
  323. package/dist/server/server.js +752 -0
  324. package/dist/server/server.js.map +1 -0
  325. package/dist/services/spine-services.d.ts +88 -0
  326. package/dist/services/spine-services.d.ts.map +1 -0
  327. package/dist/services/spine-services.js +1586 -0
  328. package/dist/services/spine-services.js.map +1 -0
  329. package/dist/stand/stand.d.ts +294 -0
  330. package/dist/stand/stand.d.ts.map +1 -0
  331. package/dist/stand/stand.js +680 -0
  332. package/dist/stand/stand.js.map +1 -0
  333. package/dist/stand/subscription-observer.d.ts +53 -0
  334. package/dist/stand/subscription-observer.d.ts.map +1 -0
  335. package/dist/stand/subscription-observer.js +444 -0
  336. package/dist/stand/subscription-observer.js.map +1 -0
  337. package/dist/stand/subscription-records.d.ts +12 -0
  338. package/dist/stand/subscription-records.d.ts.map +1 -0
  339. package/dist/stand/subscription-records.js +99 -0
  340. package/dist/stand/subscription-records.js.map +1 -0
  341. package/dist/stand/subscription-registry.d.ts +285 -0
  342. package/dist/stand/subscription-registry.d.ts.map +1 -0
  343. package/dist/stand/subscription-registry.js +403 -0
  344. package/dist/stand/subscription-registry.js.map +1 -0
  345. package/dist/stand/subscription-runtime.d.ts +102 -0
  346. package/dist/stand/subscription-runtime.d.ts.map +1 -0
  347. package/dist/stand/subscription-runtime.js +380 -0
  348. package/dist/stand/subscription-runtime.js.map +1 -0
  349. package/dist/testing/index.d.ts +14 -0
  350. package/dist/testing/index.d.ts.map +1 -0
  351. package/dist/testing/index.js +34 -0
  352. package/dist/testing/index.js.map +1 -0
  353. package/dist/tsconfig.tsbuildinfo +1 -0
  354. package/package.json +65 -0
@@ -0,0 +1,1586 @@
1
+ /*
2
+ * Copyright 2026, CodeMatters. All rights reserved.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5
+ * in compliance with the License. You may obtain a copy of the License at
6
+ *
7
+ * https://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
10
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11
+ * or implied. See the License for the specific language governing permissions and limitations under
12
+ * the License.
13
+ */
14
+ import { randomUUID } from "node:crypto";
15
+ import { Code, ConnectError } from "@connectrpc/connect";
16
+ import { clone, create, ScalarType, toBinary, } from "@bufbuild/protobuf";
17
+ import { AnySchema, BoolValueSchema, BytesValueSchema, DoubleValueSchema, EmptySchema, FloatValueSchema, Int32ValueSchema, Int64ValueSchema, StringValueSchema, UInt32ValueSchema, UInt64ValueSchema, } from "@bufbuild/protobuf/wkt";
18
+ import { TypeUrls, AnyMessages } from "@spine-event-engine/core";
19
+ import { CommandIdSchema, EventSchema, TenantIdSchema, ValidationErrorSchema, VersionSchema, } from "@spine-event-engine/proto";
20
+ import { ErrorSchema } from "@spine-event-engine/proto";
21
+ import { CommandService } from "@spine-event-engine/proto/client";
22
+ import { CompositeFilter_CompositeOperator, Filter_Operator, } from "@spine-event-engine/proto/client";
23
+ import { EntityStateWithVersionSchema as EntityStateVersionSchema, OrderBy_Direction, QueryResponseSchema, } from "@spine-event-engine/proto/client";
24
+ import { QueryService } from "@spine-event-engine/proto/client";
25
+ import { EntityStateUpdateSchema, EntityUpdatesSchema, EventUpdatesSchema, SubscriptionIdSchema, SubscriptionSchema, SubscriptionUpdateSchema, TopicSchema, } from "@spine-event-engine/proto/client";
26
+ import { SubscriptionService } from "@spine-event-engine/proto/client";
27
+ import { AckSchema } from "@spine-event-engine/proto";
28
+ import { ResponseSchema, StatusSchema, } from "@spine-event-engine/proto";
29
+ import { RecordMask, TenantBoundary, } from "@spine-event-engine/storage";
30
+ import { boundedContextAccess } from "../context/bounded-context.js";
31
+ import { CommandValidationError } from "../bus/command-errors.js";
32
+ import { TransitionValidationError } from "../repository/command-errors.js";
33
+ import {} from "../stand/stand.js";
34
+ import { emitServerWarning } from "../server/server-log.js";
35
+ import { managedChildSubscriptionAccess } from "../server/managed-child-subscription.js";
36
+ import { InMemorySubscriptionRegistry, } from "../stand/subscription-registry.js";
37
+ const serviceLoggers = new WeakMap();
38
+ const serviceInstances = new WeakSet();
39
+ /**
40
+ * Small route registrar for the first public Spine gRPC service slice.
41
+ *
42
+ * `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.
53
+ *
54
+ * `SubscriptionService.Subscribe` accepts known state targets with
55
+ * `include_all` or validated ID/field filters and known event targets exposed
56
+ * by built-context event dispatchers with `include_all = true`. It stores the
57
+ * canonical definition in the bounded context's subscription registry and
58
+ * attaches delivery only when the opaque subscription ID is activated: state
59
+ * subscriptions attach to `Stand`, while
60
+ * event subscriptions attach to a framework-internal `EventBus` listener.
61
+ * 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`
64
+ * containing cloned framework `Event` envelopes; rejection updates omit
65
+ * rejected-command payload forms and stack trace from their client-facing
66
+ * context.
67
+ * Application code remains on generated domain event messages through handler
68
+ * dispatch. Unknown or duplicate activation IDs complete without updates.
69
+ * Cancellation of unknown or already-cleaned IDs returns OK after admission to
70
+ * the bounded unknown-removal pool.
71
+ */
72
+ export class SpineServices {
73
+ #contexts;
74
+ #commandRoutes = new Map();
75
+ #stateRoutes = new Map();
76
+ #eventRoutes = new Map();
77
+ #subscriptions = new Map();
78
+ #activationTails = new Map();
79
+ #removals = new Map();
80
+ #unknownRemovals = new Set();
81
+ #testRegistries = new WeakMap();
82
+ #queueLimit;
83
+ #subscriptionLimit;
84
+ /**
85
+ * Creates service adapters over built bounded contexts.
86
+ *
87
+ * @param options Configures contexts and subscription bounds.
88
+ */
89
+ constructor(options) {
90
+ serviceInstances.add(this);
91
+ this.#contexts = Object.freeze([...options.contexts]);
92
+ this.#queueLimit = ServiceValues.positiveInteger(options.queueLimit ?? ServiceValues.defaultQueueLimit);
93
+ this.#subscriptionLimit = ServiceValues.subscriptionLimit(options.subscriptionLimit ?? ServiceValues.defaultSubscriptionLimit);
94
+ for (const context of this.#contexts) {
95
+ for (const typeUrl of context.commandBus().acceptedCommandTypes()) {
96
+ if (!this.#commandRoutes.has(typeUrl)) {
97
+ this.#commandRoutes.set(typeUrl, { context, typeUrl });
98
+ }
99
+ }
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
+ for (const typeUrl of context.eventBus().acceptedEventTypes()) {
120
+ if (!this.#eventRoutes.has(typeUrl)) {
121
+ this.#eventRoutes.set(typeUrl, { context, kind: "event", typeUrl });
122
+ }
123
+ }
124
+ }
125
+ }
126
+ /**
127
+ * Registers CommandService, QueryService, and SubscriptionService routes.
128
+ *
129
+ * @param router Receives the Connect service procedures.
130
+ * @returns Returns the registered router.
131
+ */
132
+ register(router) {
133
+ router.service(CommandService, {
134
+ post: (command) => this.#post(command),
135
+ });
136
+ router.service(QueryService, {
137
+ read: (query) => this.#read(query),
138
+ });
139
+ router.service(SubscriptionService, {
140
+ subscribe: (topic) => this.#subscribe(topic),
141
+ activate: (subscription) => this.#activate(subscription),
142
+ cancel: (subscription) => this.#cancel(subscription),
143
+ });
144
+ return router;
145
+ }
146
+ async #post(command) {
147
+ const messageId = command.id && AnyMessages.pack(CommandIdSchema, command.id, { validate: false });
148
+ const typeUrl = command.message?.typeUrl;
149
+ if (typeUrl === undefined || typeUrl.length === 0) {
150
+ return create(AckSchema, {
151
+ messageId,
152
+ status: ServiceValues.errorStatus("INVALID_COMMAND", "Command message type is required."),
153
+ });
154
+ }
155
+ const route = this.#commandRoutes.get(typeUrl);
156
+ if (route === undefined) {
157
+ return create(AckSchema, {
158
+ messageId,
159
+ status: ServiceValues.errorStatus("UNSUPPORTED_COMMAND", "No bounded context accepted the command."),
160
+ });
161
+ }
162
+ const tenantError = ServiceValues.tenantMismatch(route.context.isMultitenant, ServiceValues.commandTenant(command), "command");
163
+ if (tenantError !== undefined) {
164
+ return create(AckSchema, {
165
+ messageId,
166
+ status: ServiceValues.errorStatus(tenantError.type, tenantError.message),
167
+ });
168
+ }
169
+ try {
170
+ await route.context.commandBus().post(command);
171
+ return create(AckSchema, {
172
+ messageId,
173
+ status: ServiceValues.okStatus(),
174
+ });
175
+ }
176
+ catch (error) {
177
+ const postError = ServiceValues.commandPostError(error);
178
+ return create(AckSchema, {
179
+ messageId,
180
+ status: ServiceValues.errorStatus(postError.type, postError.message, postError.details),
181
+ });
182
+ }
183
+ }
184
+ async #read(query) {
185
+ const target = query.target;
186
+ const route = this.#readRoute(target);
187
+ if (target === undefined || route === undefined) {
188
+ return ServiceValues.queryErrorResponse("UNSUPPORTED_QUERY_TARGET", "No bounded context owns query target.");
189
+ }
190
+ const queryError = ServiceValues.validateReadQuery(query, target, route);
191
+ if (queryError !== undefined) {
192
+ return ServiceValues.queryErrorResponse(queryError.type, queryError.message);
193
+ }
194
+ const tenantId = ServiceValues.tenantValue(query.context?.tenantId);
195
+ const tenantError = ServiceValues.tenantMismatch(route.context.isMultitenant, tenantId, "query");
196
+ if (tenantError !== undefined) {
197
+ return ServiceValues.queryErrorResponse(tenantError.type, tenantError.message);
198
+ }
199
+ try {
200
+ return await this.#query(route, ServiceValues.createReadPlan(target, query, route), tenantId);
201
+ }
202
+ catch {
203
+ return ServiceValues.queryErrorResponse("QUERY_READ_ERROR", "Query read failed.");
204
+ }
205
+ }
206
+ #readRoute(target) {
207
+ return target === undefined ? undefined : this.#stateRoutes.get(target.type);
208
+ }
209
+ 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));
217
+ return create(QueryResponseSchema, {
218
+ response: ServiceValues.okResponse(),
219
+ message: results.map((result) => ServiceValues.packVersionedState(route.schema, result)),
220
+ });
221
+ }
222
+ #subscribe(topic) {
223
+ const route = this.#subscriptionRoute(topic);
224
+ ServiceValues.validateTopic(topic);
225
+ const shape = ServiceValues.createSubscriptionShape(topic, route);
226
+ const tenantId = ServiceValues.topicTenant(topic);
227
+ const tenantError = ServiceValues.tenantMismatch(route.context.isMultitenant, tenantId, "subscription");
228
+ if (tenantError !== undefined) {
229
+ throw new ConnectError(tenantError.message, Code.InvalidArgument);
230
+ }
231
+ const subscription = create(SubscriptionSchema, {
232
+ id: create(SubscriptionIdSchema, { value: `s-${randomUUID()}` }),
233
+ topic: clone(TopicSchema, topic),
234
+ });
235
+ const id = subscription.id?.value;
236
+ if (id === undefined) {
237
+ throw new ConnectError("Subscription ID is required.", Code.InvalidArgument);
238
+ }
239
+ void shape;
240
+ const registry = this.#subscriptionRegistry(route.context);
241
+ if (registry === undefined) {
242
+ throw new ConnectError("Subscription registry is unavailable.", Code.FailedPrecondition);
243
+ }
244
+ return registry.create(subscription).then(() => clone(SubscriptionSchema, subscription));
245
+ }
246
+ async *#activate(subscription) {
247
+ const id = subscription.id?.value;
248
+ if (id === undefined) {
249
+ return;
250
+ }
251
+ let record;
252
+ await this.#serializeActivation(id, async () => {
253
+ const local = this.#subscriptions.get(id);
254
+ record = local ?? (await this.#findSubscription(id));
255
+ if (record === undefined)
256
+ return;
257
+ if (record.delivery.active) {
258
+ record = undefined;
259
+ return;
260
+ }
261
+ const activation = await this.#subscriptionRegistry(record.route.context)?.activate(create(SubscriptionIdSchema, { value: id }));
262
+ if (activation === undefined ||
263
+ activation.kind === "missing" ||
264
+ activation.kind === "expired" ||
265
+ record.delivery.closed) {
266
+ record = undefined;
267
+ return;
268
+ }
269
+ try {
270
+ this.#subscriptions.set(id, record);
271
+ await this.#activateRecord(record);
272
+ }
273
+ catch (error) {
274
+ try {
275
+ await this.#removeSubscription(id);
276
+ }
277
+ catch (cleanupError) {
278
+ throw new AggregateError([error, cleanupError], "Subscription activation and cleanup failed.");
279
+ }
280
+ throw error;
281
+ }
282
+ });
283
+ if (record === undefined)
284
+ return;
285
+ try {
286
+ for (;;) {
287
+ const update = await record.delivery.next();
288
+ if (update === undefined) {
289
+ return;
290
+ }
291
+ yield update;
292
+ }
293
+ }
294
+ finally {
295
+ if (!record.delivery.closed) {
296
+ await this.#removeSubscription(id);
297
+ }
298
+ }
299
+ }
300
+ async #serializeActivation(id, work) {
301
+ const previous = this.#activationTails.get(id) ?? Promise.resolve();
302
+ let finish = () => undefined;
303
+ const settled = new Promise((resolve) => {
304
+ finish = resolve;
305
+ });
306
+ const tail = previous.then(() => settled);
307
+ this.#activationTails.set(id, tail);
308
+ await previous;
309
+ try {
310
+ await work();
311
+ }
312
+ finally {
313
+ finish();
314
+ if (this.#activationTails.get(id) === tail)
315
+ this.#activationTails.delete(id);
316
+ }
317
+ }
318
+ #cancel(subscription) {
319
+ const id = subscription.id?.value;
320
+ if (id === undefined) {
321
+ return ServiceValues.okResponse();
322
+ }
323
+ return this.#removeSubscription(id).then(() => ServiceValues.okResponse());
324
+ }
325
+ #subscriptionRoute(topic) {
326
+ const target = topic.target;
327
+ const route = target === undefined
328
+ ? undefined
329
+ : (this.#stateRoutes.get(target.type) ?? this.#eventRoutes.get(target.type));
330
+ if (target === undefined || route === undefined) {
331
+ throw new ConnectError("Unsupported subscription target.", Code.InvalidArgument);
332
+ }
333
+ return route;
334
+ }
335
+ async #activateRecord(record) {
336
+ const registry = this.#subscriptionRegistry(record.route.context);
337
+ if (registry === undefined) {
338
+ this.#createSubscriptionAttachment(record);
339
+ managedChildSubscriptionAccess.installed(record.id);
340
+ return;
341
+ }
342
+ try {
343
+ const consumer = await boundedContextAccess.consumeSubscription(record.route.context, record.id, (update) => {
344
+ record.delivery.push(update);
345
+ if (record.delivery.closed) {
346
+ this.#removeDetachedSubscription(record.id);
347
+ }
348
+ });
349
+ record.delivery.attach(consumer);
350
+ }
351
+ catch (error) {
352
+ if (!(error instanceof TypeError) ||
353
+ (!error.message.includes("requires a Stand instance") &&
354
+ !error.message.includes("requires a built BoundedContext instance"))) {
355
+ throw error;
356
+ }
357
+ this.#createSubscriptionAttachment(record);
358
+ }
359
+ managedChildSubscriptionAccess.installed(record.id);
360
+ }
361
+ #createSubscriptionAttachment(record) {
362
+ if (record.kind === "event") {
363
+ const eventSubscription = boundedContextAccess.subscribeToEvent(record.route.context, record.route.typeUrl, {
364
+ onEvent: (event) => {
365
+ if (!ServiceValues.eventTenantMatches(record, event)) {
366
+ return;
367
+ }
368
+ record.delivery.push(ServiceValues.createEventUpdate(record, event));
369
+ if (record.delivery.closed) {
370
+ this.#removeDetachedSubscription(record.id);
371
+ }
372
+ },
373
+ });
374
+ record.delivery.attach(eventSubscription);
375
+ return eventSubscription;
376
+ }
377
+ const stateRecord = record;
378
+ const standSubscription = stateRecord.route.context.stand().subscribe(stateRecord.route.schema, (update) => {
379
+ const subscriptionUpdate = ServiceValues.createEntityUpdate(stateRecord, update);
380
+ if (subscriptionUpdate !== undefined) {
381
+ stateRecord.delivery.push(subscriptionUpdate);
382
+ }
383
+ if (stateRecord.delivery.closed) {
384
+ this.#removeDetachedSubscription(stateRecord.id);
385
+ }
386
+ }, ServiceValues.tenantOptions(stateRecord.tenantId));
387
+ stateRecord.delivery.attach(standSubscription);
388
+ return standSubscription;
389
+ }
390
+ #removeSubscription(id) {
391
+ const existing = this.#removals.get(id);
392
+ if (existing !== undefined) {
393
+ return existing.settled;
394
+ }
395
+ const record = this.#subscriptions.get(id);
396
+ const unknown = record === undefined;
397
+ if (unknown && this.#unknownRemovals.size >= this.#subscriptionLimit) {
398
+ return Promise.reject(new ConnectError("Subscription cancellation capacity is exhausted.", Code.ResourceExhausted));
399
+ }
400
+ if (unknown) {
401
+ this.#unknownRemovals.add(id);
402
+ }
403
+ else {
404
+ record.delivery.close();
405
+ this.#subscriptions.delete(id);
406
+ }
407
+ const removal = ServiceValues.createSubscriptionRemoval();
408
+ this.#removals.set(id, removal);
409
+ void this.#runRemoval(id, removal, record, unknown);
410
+ return removal.settled;
411
+ }
412
+ #removeDetachedSubscription(id) {
413
+ // spine-log-boundary: server.subscription_cleanup_failure
414
+ void this.#removeSubscription(id).catch(() => {
415
+ const logger = serviceLoggers.get(this);
416
+ if (logger !== undefined) {
417
+ emitServerWarning(logger, "Subscription cleanup failed.", {
418
+ subscriptionId: id,
419
+ operation: "service.subscription_cleanup",
420
+ reasonCode: "cleanup_failed",
421
+ });
422
+ }
423
+ });
424
+ }
425
+ async #runRemoval(id, removal, record, unknown) {
426
+ try {
427
+ if (record !== undefined) {
428
+ await this.#subscriptionRegistry(record.route.context)?.delete(create(SubscriptionIdSchema, { value: id }));
429
+ }
430
+ else {
431
+ await Promise.all(ServiceValues.uniqueContexts(this.#contexts).flatMap((context) => {
432
+ const registry = this.#subscriptionRegistry(context);
433
+ return registry === undefined
434
+ ? []
435
+ : [registry.delete(create(SubscriptionIdSchema, { value: id }))];
436
+ }));
437
+ }
438
+ removal.resolve();
439
+ }
440
+ catch (error) {
441
+ removal.reject(error);
442
+ }
443
+ finally {
444
+ if (unknown) {
445
+ this.#unknownRemovals.delete(id);
446
+ }
447
+ if (this.#removals.get(id) === removal) {
448
+ this.#removals.delete(id);
449
+ }
450
+ }
451
+ }
452
+ async #findSubscription(id) {
453
+ const subscriptionId = create(SubscriptionIdSchema, { value: id });
454
+ for (const context of ServiceValues.uniqueContexts(this.#contexts)) {
455
+ const entry = await this.#subscriptionRegistry(context)?.get(subscriptionId);
456
+ const subscription = entry?.subscription;
457
+ const topic = subscription?.topic;
458
+ if (entry === undefined || subscription === undefined || topic === undefined)
459
+ continue;
460
+ const canonicalTopic = clone(TopicSchema, topic);
461
+ const route = this.#subscriptionRoute(canonicalTopic);
462
+ if (route.context !== context)
463
+ continue;
464
+ const tenantId = ServiceValues.topicTenant(canonicalTopic);
465
+ if (ServiceValues.tenantMismatch(context.isMultitenant, tenantId, "subscription") !== undefined) {
466
+ continue;
467
+ }
468
+ return ServiceValues.createSubscriptionRecord({
469
+ id,
470
+ subscription: clone(SubscriptionSchema, subscription),
471
+ shape: ServiceValues.createSubscriptionShape(canonicalTopic, route),
472
+ tenantId,
473
+ queueLimit: this.#queueLimit,
474
+ });
475
+ }
476
+ return undefined;
477
+ }
478
+ #subscriptionRegistry(context) {
479
+ if (context === undefined) {
480
+ return undefined;
481
+ }
482
+ try {
483
+ return boundedContextAccess.subscriptionRegistry(context);
484
+ }
485
+ catch {
486
+ // Direct route tests may use structural context doubles. Production
487
+ // contexts always provide their configured registry through the access seam.
488
+ const key = context;
489
+ let registry = this.#testRegistries.get(key);
490
+ if (registry === undefined) {
491
+ registry = new InMemorySubscriptionRegistry();
492
+ this.#testRegistries.set(key, registry);
493
+ }
494
+ return registry;
495
+ }
496
+ }
497
+ }
498
+ /**
499
+ * Provides package-private service configuration seams.
500
+ *
501
+ * @internal
502
+ */
503
+ export const spineServicesAccess = Object.freeze({
504
+ clearLogger(services) {
505
+ if (!serviceInstances.has(services)) {
506
+ throw new TypeError("SpineServices logger requires a SpineServices instance.");
507
+ }
508
+ serviceLoggers.delete(services);
509
+ },
510
+ installLogger(services, logger) {
511
+ if (!serviceInstances.has(services)) {
512
+ throw new TypeError("SpineServices logger requires a SpineServices instance.");
513
+ }
514
+ serviceLoggers.set(services, logger);
515
+ },
516
+ });
517
+ class SubscriptionDelivery {
518
+ #queue = [];
519
+ #waiters = [];
520
+ #queueLimit;
521
+ #subscription;
522
+ #closed = false;
523
+ constructor(queueLimit) {
524
+ this.#queueLimit = queueLimit;
525
+ }
526
+ get active() {
527
+ return this.#subscription !== undefined;
528
+ }
529
+ get closed() {
530
+ return this.#closed;
531
+ }
532
+ attach(subscription) {
533
+ if (this.#closed) {
534
+ subscription.unsubscribe();
535
+ return;
536
+ }
537
+ this.#subscription = subscription;
538
+ }
539
+ push(update) {
540
+ if (this.#closed) {
541
+ return;
542
+ }
543
+ const waiter = this.#waiters.shift();
544
+ if (waiter === undefined) {
545
+ if (this.#queue.length >= this.#queueLimit) {
546
+ this.close();
547
+ return;
548
+ }
549
+ this.#queue.push(update);
550
+ }
551
+ else {
552
+ waiter(update);
553
+ }
554
+ }
555
+ next() {
556
+ const update = this.#queue.shift();
557
+ if (update !== undefined || this.#closed) {
558
+ return Promise.resolve(update);
559
+ }
560
+ return new Promise((resolve) => this.#waiters.push(resolve));
561
+ }
562
+ close() {
563
+ if (this.#closed) {
564
+ return;
565
+ }
566
+ this.#closed = true;
567
+ this.#queue.length = 0;
568
+ this.#subscription?.unsubscribe();
569
+ this.#subscription = undefined;
570
+ for (const waiter of this.#waiters.splice(0)) {
571
+ waiter(undefined);
572
+ }
573
+ }
574
+ }
575
+ /**
576
+ * Groups non-public service contract, query, and subscription operations.
577
+ */
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() });
601
+ }
602
+ function errorResponse(type, message) {
603
+ return create(ResponseSchema, { status: errorStatus(type, message) });
604
+ }
605
+ function queryErrorResponse(type, message) {
606
+ return create(QueryResponseSchema, {
607
+ response: errorResponse(type, message),
608
+ });
609
+ }
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
+ }
626
+ }
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;
653
+ }
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;
699
+ }
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}.`);
707
+ }
708
+ return undefined;
709
+ }
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;
752
+ }
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
+ }
770
+ }
771
+ if (format.limit > QueryLimits.resultCount) {
772
+ return {
773
+ type: "INVALID_QUERY",
774
+ message: "QueryService.Read limit may be at most 1000.",
775
+ };
776
+ }
777
+ if (format.limit > 0 && format.orderBy.length === 0) {
778
+ return {
779
+ type: "INVALID_QUERY",
780
+ message: "QueryService.Read limit requires ordering.",
781
+ };
782
+ }
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
+ }
798
+ }
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.`);
886
+ }
887
+ 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);
951
+ }
952
+ if (column === "archived" || column === "deleted") {
953
+ return (value.typeUrl === TypeUrls.derive(BoolValueSchema) &&
954
+ AnyMessages.unpack(value, BoolValueSchema) !== undefined);
955
+ }
956
+ return false;
957
+ }
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";
978
+ return undefined;
979
+ }
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.`);
997
+ }
998
+ function unsupportedFilterError(message) {
999
+ return invalidQueryError(message);
1000
+ }
1001
+ function unsupportedFormatError(message) {
1002
+ return invalidQueryError(message);
1003
+ }
1004
+ function invalidQueryError(message) {
1005
+ return {
1006
+ type: "INVALID_QUERY",
1007
+ message,
1008
+ };
1009
+ }
1010
+ function invalidCriterionError() {
1011
+ return {
1012
+ type: "INVALID_QUERY",
1013
+ message: "QueryService.Read requires filters or include_all = true.",
1014
+ };
1015
+ }
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,
1042
+ });
1043
+ function positiveInteger(value) {
1044
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : 1;
1045
+ }
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;
1051
+ }
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
+ };
1060
+ }
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 };
1071
+ }
1072
+ function uniqueContexts(contexts) {
1073
+ return [...new Set(contexts)];
1074
+ }
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
+ }
1085
+ }
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:
1109
+ throw new ConnectError("SubscriptionService.Subscribe requires filters or include_all = true.", Code.InvalidArgument);
1110
+ }
1111
+ }
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:
1125
+ throw new ConnectError("SubscriptionService.Subscribe requires filters or include_all = true.", Code.InvalidArgument);
1126
+ }
1127
+ }
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
+ }
1154
+ 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
+ }
1179
+ validateSubscriptionFilterNode(current.filter, current.depth, counts);
1180
+ enqueueSubscriptionFilterChildren(current.filter, current.depth, stack);
1181
+ }
1182
+ }
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
+ }
1209
+ }
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
+ }
1217
+ }
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));
1229
+ }
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);
1241
+ }
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
+ });
1258
+ }
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
+ }
1269
+ }
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);
1280
+ }
1281
+ return decoded;
1282
+ }
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;
1292
+ }
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;
1310
+ }
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
+ });
1331
+ }
1332
+ function findMessageField(schema, name) {
1333
+ const fields = (schema?.fields ?? []);
1334
+ return fields.find((field) => field.name === name || field.localName === name);
1335
+ }
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;
1345
+ }
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);
1357
+ }
1358
+ function isMessage(value) {
1359
+ return typeof value === "object" && value !== null && "$typeName" in value;
1360
+ }
1361
+ function isAny(value) {
1362
+ return isMessage(value) && value.$typeName === "google.protobuf.Any";
1363
+ }
1364
+ function bytesEqual(left, right) {
1365
+ return left.length === right.length && left.every((byte, index) => byte === right[index]);
1366
+ }
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
+ });
1372
+ }
1373
+ function commandTenant(command) {
1374
+ return tenantValue(command.context?.actorContext?.tenantId);
1375
+ }
1376
+ function topicTenant(topic) {
1377
+ return tenantValue(topic?.context?.tenantId);
1378
+ }
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;
1387
+ }
1388
+ }
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));
1394
+ }
1395
+ function tenantValue(tenantId) {
1396
+ if (tenantId === undefined)
1397
+ return undefined;
1398
+ try {
1399
+ return TenantBoundary.from(tenantId).tenantId;
1400
+ }
1401
+ catch {
1402
+ return undefined;
1403
+ }
1404
+ }
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;
1414
+ }
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
+ }
1424
+ return tenantId === undefined
1425
+ ? undefined
1426
+ : {
1427
+ type: "TENANT_INAPPLICABLE",
1428
+ message: `Tenant is not applicable for this ${subject}.`,
1429
+ };
1430
+ }
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
+ }
1450
+ return {
1451
+ type: "COMMAND_POST_ERROR",
1452
+ message: "Command post failed.",
1453
+ };
1454
+ }
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
+ });
1502
+ }
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
+ });
1514
+ }
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;
1554
+ }
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
+ })();
1586
+ //# sourceMappingURL=spine-services.js.map