@cratis/arc 22.150.0 → 22.152.0

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 (193) hide show
  1. package/Globals.ts +10 -0
  2. package/dist/cjs/Globals.d.ts +2 -0
  3. package/dist/cjs/Globals.d.ts.map +1 -1
  4. package/dist/cjs/Globals.js +7 -0
  5. package/dist/cjs/Globals.js.map +1 -1
  6. package/dist/cjs/queries/ObservableQueryConnectionFactory.d.ts +5 -0
  7. package/dist/cjs/queries/ObservableQueryConnectionFactory.d.ts.map +1 -1
  8. package/dist/cjs/queries/ObservableQueryConnectionFactory.js +16 -1
  9. package/dist/cjs/queries/ObservableQueryConnectionFactory.js.map +1 -1
  10. package/dist/cjs/queries/ObservableQueryFor.d.ts +3 -0
  11. package/dist/cjs/queries/ObservableQueryFor.d.ts.map +1 -1
  12. package/dist/cjs/queries/ObservableQueryFor.js +12 -3
  13. package/dist/cjs/queries/ObservableQueryFor.js.map +1 -1
  14. package/dist/cjs/queries/PollingOptions.d.ts +8 -0
  15. package/dist/cjs/queries/PollingOptions.d.ts.map +1 -0
  16. package/dist/cjs/queries/PollingQueryConnection.d.ts +33 -0
  17. package/dist/cjs/queries/PollingQueryConnection.d.ts.map +1 -0
  18. package/dist/cjs/queries/PollingQueryConnection.js +150 -0
  19. package/dist/cjs/queries/PollingQueryConnection.js.map +1 -0
  20. package/dist/cjs/queries/QueryTransportMethod.d.ts +2 -1
  21. package/dist/cjs/queries/QueryTransportMethod.d.ts.map +1 -1
  22. package/dist/cjs/queries/QueryTransportMethod.js +1 -0
  23. package/dist/cjs/queries/QueryTransportMethod.js.map +1 -1
  24. package/dist/cjs/queries/index.d.ts +2 -0
  25. package/dist/cjs/queries/index.d.ts.map +1 -1
  26. package/dist/cjs/queries/index.js +4 -0
  27. package/dist/cjs/queries/index.js.map +1 -1
  28. package/dist/esm/Globals.d.ts +2 -0
  29. package/dist/esm/Globals.d.ts.map +1 -1
  30. package/dist/esm/Globals.js +7 -0
  31. package/dist/esm/Globals.js.map +1 -1
  32. package/dist/esm/queries/ObservableQueryConnectionFactory.d.ts +5 -0
  33. package/dist/esm/queries/ObservableQueryConnectionFactory.d.ts.map +1 -1
  34. package/dist/esm/queries/ObservableQueryConnectionFactory.js +16 -2
  35. package/dist/esm/queries/ObservableQueryConnectionFactory.js.map +1 -1
  36. package/dist/esm/queries/ObservableQueryFor.d.ts +3 -0
  37. package/dist/esm/queries/ObservableQueryFor.d.ts.map +1 -1
  38. package/dist/esm/queries/ObservableQueryFor.js +13 -4
  39. package/dist/esm/queries/ObservableQueryFor.js.map +1 -1
  40. package/dist/esm/queries/PollingOptions.d.ts +8 -0
  41. package/dist/esm/queries/PollingOptions.d.ts.map +1 -0
  42. package/dist/esm/queries/PollingOptions.js +2 -0
  43. package/dist/esm/queries/PollingOptions.js.map +1 -0
  44. package/dist/esm/queries/PollingQueryConnection.d.ts +33 -0
  45. package/dist/esm/queries/PollingQueryConnection.d.ts.map +1 -0
  46. package/dist/esm/queries/PollingQueryConnection.js +147 -0
  47. package/dist/esm/queries/PollingQueryConnection.js.map +1 -0
  48. package/dist/esm/queries/QueryTransportMethod.d.ts +2 -1
  49. package/dist/esm/queries/QueryTransportMethod.d.ts.map +1 -1
  50. package/dist/esm/queries/QueryTransportMethod.js +1 -0
  51. package/dist/esm/queries/QueryTransportMethod.js.map +1 -1
  52. package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_direct_mode_and_polling_transport.d.ts +2 -0
  53. package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_direct_mode_and_polling_transport.d.ts.map +1 -0
  54. package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_direct_mode_and_polling_transport.js +46 -0
  55. package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_direct_mode_and_polling_transport.js.map +1 -0
  56. package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_polling_transport.d.ts +2 -0
  57. package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_polling_transport.d.ts.map +1 -0
  58. package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_polling_transport.js +46 -0
  59. package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_polling_transport.js.map +1 -0
  60. package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_polling_transport.d.ts +2 -0
  61. package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_polling_transport.d.ts.map +1 -0
  62. package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_polling_transport.js +108 -0
  63. package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_polling_transport.js.map +1 -0
  64. package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_polling_transport_and_unauthorized_response.d.ts +2 -0
  65. package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_polling_transport_and_unauthorized_response.d.ts.map +1 -0
  66. package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_polling_transport_and_unauthorized_response.js +39 -0
  67. package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_polling_transport_and_unauthorized_response.js.map +1 -0
  68. package/dist/esm/queries/for_PollingQueryConnection/given/a_polling_query_connection.d.ts +31 -0
  69. package/dist/esm/queries/for_PollingQueryConnection/given/a_polling_query_connection.d.ts.map +1 -0
  70. package/dist/esm/queries/for_PollingQueryConnection/given/a_polling_query_connection.js +52 -0
  71. package/dist/esm/queries/for_PollingQueryConnection/given/a_polling_query_connection.js.map +1 -0
  72. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/after_a_network_error.d.ts +2 -0
  73. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/after_a_network_error.d.ts.map +1 -0
  74. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/after_a_network_error.js +16 -0
  75. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/after_a_network_error.js.map +1 -0
  76. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/after_a_server_error_with_an_envelope.d.ts +2 -0
  77. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/after_a_server_error_with_an_envelope.d.ts.map +1 -0
  78. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/after_a_server_error_with_an_envelope.js +22 -0
  79. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/after_a_server_error_with_an_envelope.js.map +1 -0
  80. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/after_not_ready.d.ts +2 -0
  81. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/after_not_ready.d.ts.map +1 -0
  82. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/after_not_ready.js +19 -0
  83. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/after_not_ready.js.map +1 -0
  84. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/after_repeated_failures.d.ts +2 -0
  85. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/after_repeated_failures.d.ts.map +1 -0
  86. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/after_repeated_failures.js +16 -0
  87. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/after_repeated_failures.js.map +1 -0
  88. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/after_request_timeout.d.ts +2 -0
  89. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/after_request_timeout.d.ts.map +1 -0
  90. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/after_request_timeout.js +19 -0
  91. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/after_request_timeout.js.map +1 -0
  92. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/with_jitter.d.ts +2 -0
  93. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/with_jitter.d.ts.map +1 -0
  94. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/with_jitter.js +16 -0
  95. package/dist/esm/queries/for_PollingQueryConnection/when_backing_off/with_jitter.js.map +1 -0
  96. package/dist/esm/queries/for_PollingQueryConnection/when_disconnecting/and_fetch_rejects_on_abort.d.ts +2 -0
  97. package/dist/esm/queries/for_PollingQueryConnection/when_disconnecting/and_fetch_rejects_on_abort.d.ts.map +1 -0
  98. package/dist/esm/queries/for_PollingQueryConnection/when_disconnecting/and_fetch_rejects_on_abort.js +16 -0
  99. package/dist/esm/queries/for_PollingQueryConnection/when_disconnecting/and_fetch_rejects_on_abort.js.map +1 -0
  100. package/dist/esm/queries/for_PollingQueryConnection/when_disconnecting/while_a_request_is_in_flight.d.ts +2 -0
  101. package/dist/esm/queries/for_PollingQueryConnection/when_disconnecting/while_a_request_is_in_flight.d.ts.map +1 -0
  102. package/dist/esm/queries/for_PollingQueryConnection/when_disconnecting/while_a_request_is_in_flight.js +16 -0
  103. package/dist/esm/queries/for_PollingQueryConnection/when_disconnecting/while_a_request_is_in_flight.js.map +1 -0
  104. package/dist/esm/queries/for_PollingQueryConnection/when_disconnecting/while_waiting_for_the_next_poll.d.ts +2 -0
  105. package/dist/esm/queries/for_PollingQueryConnection/when_disconnecting/while_waiting_for_the_next_poll.d.ts.map +1 -0
  106. package/dist/esm/queries/for_PollingQueryConnection/when_disconnecting/while_waiting_for_the_next_poll.js +14 -0
  107. package/dist/esm/queries/for_PollingQueryConnection/when_disconnecting/while_waiting_for_the_next_poll.js.map +1 -0
  108. package/dist/esm/queries/for_PollingQueryConnection/when_polling/and_a_request_is_still_in_flight.d.ts +2 -0
  109. package/dist/esm/queries/for_PollingQueryConnection/when_polling/and_a_request_is_still_in_flight.d.ts.map +1 -0
  110. package/dist/esm/queries/for_PollingQueryConnection/when_polling/and_a_request_is_still_in_flight.js +12 -0
  111. package/dist/esm/queries/for_PollingQueryConnection/when_polling/and_a_request_is_still_in_flight.js.map +1 -0
  112. package/dist/esm/queries/for_PollingQueryConnection/when_polling/and_data_changes.d.ts +2 -0
  113. package/dist/esm/queries/for_PollingQueryConnection/when_polling/and_data_changes.d.ts.map +1 -0
  114. package/dist/esm/queries/for_PollingQueryConnection/when_polling/and_data_changes.js +47 -0
  115. package/dist/esm/queries/for_PollingQueryConnection/when_polling/and_data_changes.js.map +1 -0
  116. package/dist/esm/queries/for_PollingQueryConnection/when_polling/and_data_is_unchanged.d.ts +2 -0
  117. package/dist/esm/queries/for_PollingQueryConnection/when_polling/and_data_is_unchanged.d.ts.map +1 -0
  118. package/dist/esm/queries/for_PollingQueryConnection/when_polling/and_data_is_unchanged.js +21 -0
  119. package/dist/esm/queries/for_PollingQueryConnection/when_polling/and_data_is_unchanged.js.map +1 -0
  120. package/dist/esm/queries/for_PollingQueryConnection/when_polling/with_global_and_instance_headers.d.ts +2 -0
  121. package/dist/esm/queries/for_PollingQueryConnection/when_polling/with_global_and_instance_headers.d.ts.map +1 -0
  122. package/dist/esm/queries/for_PollingQueryConnection/when_polling/with_global_and_instance_headers.js +25 -0
  123. package/dist/esm/queries/for_PollingQueryConnection/when_polling/with_global_and_instance_headers.js.map +1 -0
  124. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/above_the_maximum.d.ts +2 -0
  125. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/above_the_maximum.d.ts.map +1 -0
  126. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/above_the_maximum.js +16 -0
  127. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/above_the_maximum.js.map +1 -0
  128. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/below_the_minimum.d.ts +2 -0
  129. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/below_the_minimum.d.ts.map +1 -0
  130. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/below_the_minimum.js +18 -0
  131. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/below_the_minimum.js.map +1 -0
  132. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/on_a_not_ready_response.d.ts +2 -0
  133. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/on_a_not_ready_response.d.ts.map +1 -0
  134. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/on_a_not_ready_response.js +16 -0
  135. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/on_a_not_ready_response.js.map +1 -0
  136. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/that_is_invalid.d.ts +2 -0
  137. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/that_is_invalid.d.ts.map +1 -0
  138. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/that_is_invalid.js +18 -0
  139. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/that_is_invalid.js.map +1 -0
  140. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/within_bounds.d.ts +2 -0
  141. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/within_bounds.d.ts.map +1 -0
  142. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/within_bounds.js +16 -0
  143. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/within_bounds.js.map +1 -0
  144. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_invalid_arguments/stops_polling.d.ts +2 -0
  145. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_invalid_arguments/stops_polling.d.ts.map +1 -0
  146. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_invalid_arguments/stops_polling.js +12 -0
  147. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_invalid_arguments/stops_polling.js.map +1 -0
  148. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_unauthorized/stops_polling.d.ts +2 -0
  149. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_unauthorized/stops_polling.d.ts.map +1 -0
  150. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_unauthorized/stops_polling.js +14 -0
  151. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_unauthorized/stops_polling.js.map +1 -0
  152. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_unauthorized/with_a_non_json_body.d.ts +2 -0
  153. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_unauthorized/with_a_non_json_body.d.ts.map +1 -0
  154. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_unauthorized/with_a_non_json_body.js +14 -0
  155. package/dist/esm/queries/for_PollingQueryConnection/when_receiving_unauthorized/with_a_non_json_body.js.map +1 -0
  156. package/dist/esm/queries/index.d.ts +2 -0
  157. package/dist/esm/queries/index.d.ts.map +1 -1
  158. package/dist/esm/queries/index.js +2 -1
  159. package/dist/esm/queries/index.js.map +1 -1
  160. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  161. package/package.json +1 -1
  162. package/queries/ObservableQueryConnectionFactory.ts +24 -1
  163. package/queries/ObservableQueryFor.ts +15 -3
  164. package/queries/PollingOptions.ts +16 -0
  165. package/queries/PollingQueryConnection.ts +149 -0
  166. package/queries/QueryTransportMethod.ts +3 -0
  167. package/queries/for_ObservableQueryConnectionFactory/when_creating/with_direct_mode_and_polling_transport.ts +47 -0
  168. package/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_polling_transport.ts +47 -0
  169. package/queries/for_ObservableQueryFor/when_subscribing/with_polling_transport.ts +115 -0
  170. package/queries/for_ObservableQueryFor/when_subscribing/with_polling_transport_and_unauthorized_response.ts +44 -0
  171. package/queries/for_PollingQueryConnection/given/a_polling_query_connection.ts +60 -0
  172. package/queries/for_PollingQueryConnection/when_backing_off/after_a_network_error.ts +19 -0
  173. package/queries/for_PollingQueryConnection/when_backing_off/after_a_server_error_with_an_envelope.ts +25 -0
  174. package/queries/for_PollingQueryConnection/when_backing_off/after_not_ready.ts +22 -0
  175. package/queries/for_PollingQueryConnection/when_backing_off/after_repeated_failures.ts +19 -0
  176. package/queries/for_PollingQueryConnection/when_backing_off/after_request_timeout.ts +22 -0
  177. package/queries/for_PollingQueryConnection/when_backing_off/with_jitter.ts +19 -0
  178. package/queries/for_PollingQueryConnection/when_disconnecting/and_fetch_rejects_on_abort.ts +19 -0
  179. package/queries/for_PollingQueryConnection/when_disconnecting/while_a_request_is_in_flight.ts +19 -0
  180. package/queries/for_PollingQueryConnection/when_disconnecting/while_waiting_for_the_next_poll.ts +17 -0
  181. package/queries/for_PollingQueryConnection/when_polling/and_a_request_is_still_in_flight.ts +15 -0
  182. package/queries/for_PollingQueryConnection/when_polling/and_data_changes.ts +51 -0
  183. package/queries/for_PollingQueryConnection/when_polling/and_data_is_unchanged.ts +24 -0
  184. package/queries/for_PollingQueryConnection/when_polling/with_global_and_instance_headers.ts +28 -0
  185. package/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/above_the_maximum.ts +19 -0
  186. package/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/below_the_minimum.ts +21 -0
  187. package/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/on_a_not_ready_response.ts +19 -0
  188. package/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/that_is_invalid.ts +21 -0
  189. package/queries/for_PollingQueryConnection/when_receiving_a_poll_hint/within_bounds.ts +19 -0
  190. package/queries/for_PollingQueryConnection/when_receiving_invalid_arguments/stops_polling.ts +15 -0
  191. package/queries/for_PollingQueryConnection/when_receiving_unauthorized/stops_polling.ts +17 -0
  192. package/queries/for_PollingQueryConnection/when_receiving_unauthorized/with_a_non_json_body.ts +17 -0
  193. package/queries/index.ts +2 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@cratis/arc",
4
- "version": "22.150.0",
4
+ "version": "22.152.0",
5
5
  "description": "TypeScript command, query, validation, identity, and messaging runtime for Cratis Arc — the CQRS application framework for ASP.NET Core — used by generated proxies.",
6
6
  "keywords": [
7
7
  "cratis",
@@ -1,6 +1,9 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
+ import { GetHttpHeaders } from '../GetHttpHeaders.js';
5
+ import { PollingOptions } from './PollingOptions.js';
6
+ import { PollingQueryConnection } from './PollingQueryConnection.js';
4
7
  import { Globals } from '../Globals.js';
5
8
  import { joinPaths } from '../joinPaths.js';
6
9
  import { UrlHelpers } from '../UrlHelpers.js';
@@ -40,12 +43,17 @@ export interface QueryConnectionDescriptor {
40
43
  microservice: string;
41
44
  /** Route/query arguments for the current subscription. */
42
45
  args?: object;
46
+ /** Per-query custom headers used by HTTP polling. */
47
+ httpHeadersCallback?: GetHttpHeaders;
48
+ /** Per-query timing overrides for HTTP polling. */
49
+ pollingOptions?: Partial<PollingOptions>;
43
50
  }
44
51
 
45
52
  /**
46
53
  * Single entry-point for creating observable query connections.
47
54
  *
48
- * The transport is chosen as a 2x2 matrix of (direct | hub) x (WebSocket | SSE):
55
+ * Polling always uses each query's HTTP route. Persistent transports use a
56
+ * 2x2 matrix of (direct | hub) x (WebSocket | SSE):
49
57
  *
50
58
  * | | Direct (per-query URL) | Hub (centralized endpoint) |
51
59
  * |----------------|-------------------------------|-------------------------------|
@@ -58,6 +66,13 @@ export interface QueryConnectionDescriptor {
58
66
  * {@link ObservableQueryFor} never needs to know which transport is in use.
59
67
  */
60
68
  export function createObservableQueryConnection<TDataType>(descriptor: QueryConnectionDescriptor): IObservableQueryConnection<TDataType> {
69
+ if (Globals.queryTransportMethod === QueryTransportMethod.Polling) {
70
+ return new PollingQueryConnection<TDataType>(
71
+ buildDirectUrl(descriptor), descriptor.httpHeadersCallback ?? (() => ({})),
72
+ descriptor.microservice, { ...Globals.queryPollingOptions, ...descriptor.pollingOptions }
73
+ );
74
+ }
75
+
61
76
  const isSSE = Globals.queryTransportMethod === QueryTransportMethod.ServerSentEvents;
62
77
 
63
78
  if (Globals.queryDirectMode) {
@@ -69,6 +84,14 @@ export function createObservableQueryConnection<TDataType>(descriptor: QueryConn
69
84
  return createMultiplexedConnection<TDataType>(descriptor, isSSE);
70
85
  }
71
86
 
87
+ /**
88
+ * Determines whether the selected transport connects to each query's own route rather than a hub.
89
+ * @returns True for direct mode or HTTP polling.
90
+ */
91
+ export function connectsToQueryRoute(): boolean {
92
+ return Globals.queryDirectMode || Globals.queryTransportMethod === QueryTransportMethod.Polling;
93
+ }
94
+
72
95
  // ---- Direct mode: one connection per query, hitting the query's own URL ----
73
96
 
74
97
  function buildDirectUrl(descriptor: QueryConnectionDescriptor): URL {
@@ -7,7 +7,8 @@ import { ValidateRequestArguments } from './ValidateRequestArguments.js';
7
7
  import type { QueryValidator } from './QueryValidator.js';
8
8
  import type { IObservableQueryConnection } from './IObservableQueryConnection.js';
9
9
  import { NullObservableQueryConnection } from './NullObservableQueryConnection.js';
10
- import { createObservableQueryConnection } from './ObservableQueryConnectionFactory.js';
10
+ import { PollingOptions } from './PollingOptions.js';
11
+ import { createObservableQueryConnection, connectsToQueryRoute } from './ObservableQueryConnectionFactory.js';
11
12
  import type { Constructor } from '@cratis/fundamentals';
12
13
  import { deserializeQueryModel, deserializeQueryModels } from './deserializeQueryModel.js';
13
14
  import { QueryResult } from './QueryResult.js';
@@ -44,6 +45,7 @@ export abstract class ObservableQueryFor<
44
45
  private _connection?: IObservableQueryConnection<TDataType>;
45
46
  private _httpHeadersCallback: GetHttpHeaders;
46
47
  private _httpMethod?: QueryHttpMethod;
48
+ private _pollingOptions?: Partial<PollingOptions>;
47
49
 
48
50
  abstract readonly route: string;
49
51
  abstract readonly defaultValue: TDataType;
@@ -100,6 +102,14 @@ export abstract class ObservableQueryFor<
100
102
  this._httpHeadersCallback = callback;
101
103
  }
102
104
 
105
+ /**
106
+ * Overrides {@link Globals.queryPollingOptions} for this query instance on its next subscription.
107
+ * @param options The polling timing overrides to apply on the next subscribe.
108
+ */
109
+ setPollingOptions(options: Partial<PollingOptions>): void {
110
+ this._pollingOptions = options;
111
+ }
112
+
103
113
  /** @inheritdoc */
104
114
  setHttpMethod(method: QueryHttpMethod): void {
105
115
  this._httpMethod = method;
@@ -131,6 +141,8 @@ export abstract class ObservableQueryFor<
131
141
  apiBasePath: this._apiBasePath,
132
142
  microservice: this._microservice,
133
143
  args: args as object,
144
+ httpHeadersCallback: this._httpHeadersCallback,
145
+ pollingOptions: this._pollingOptions,
134
146
  });
135
147
  } else {
136
148
  this._connection = new NullObservableQueryConnection(this.defaultValue);
@@ -140,7 +152,7 @@ export abstract class ObservableQueryFor<
140
152
  // must take precedence over any stale instance property values. Spread parameterValues
141
153
  // first so that the caller-supplied args can override them.
142
154
  //
143
- // In direct mode the route arguments are already embedded in the URL path, so only
155
+ // In direct mode or polling the route arguments are already embedded in the URL path, so only
144
156
  // the unused (non-route) parameters are appended as additional query arguments.
145
157
  // In multiplexed mode ALL arguments — including route-derived ones — must be included
146
158
  // in the subscribe payload so the server can execute the query correctly.
@@ -151,7 +163,7 @@ export abstract class ObservableQueryFor<
151
163
  );
152
164
  const connectionQueryArguments: any = {
153
165
  ...parameterValues,
154
- ...(Globals.queryDirectMode ? unusedParameters : (args as object) || {}),
166
+ ...(connectsToQueryRoute() ? unusedParameters : (args as object) || {}),
155
167
  ...this.buildQueryArguments(),
156
168
  };
157
169
 
@@ -0,0 +1,16 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ /** Timing policy for HTTP observable-query polling. */
5
+ export interface PollingOptions {
6
+ /** Normal interval after a successful poll, in milliseconds. Defaults to 5000. */
7
+ intervalMs: number;
8
+ /** Minimum delay, including server hints and jitter, in milliseconds. Defaults to 1000. */
9
+ minIntervalMs: number;
10
+ /** Maximum delay, including server hints and jitter, in milliseconds. Defaults to 60000. */
11
+ maxIntervalMs: number;
12
+ /** Exponential multiplier after transient failures. Defaults to 2. */
13
+ backoffMultiplier: number;
14
+ /** Symmetric random proportion applied to backoff delays. Defaults to 0.2 (20%). */
15
+ jitterRatio: number;
16
+ }
@@ -0,0 +1,149 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { GetHttpHeaders } from '../GetHttpHeaders.js';
5
+ import { Globals } from '../Globals.js';
6
+ import { UrlHelpers } from '../UrlHelpers.js';
7
+ import { deepEqual } from '../deepEqual.js';
8
+ import { DataReceived } from './ObservableQueryConnection.js';
9
+ import { IObservableQueryConnection } from './IObservableQueryConnection.js';
10
+ import { PollingOptions } from './PollingOptions.js';
11
+ import { QueryResult } from './QueryResult.js';
12
+ import { isAbortError } from './isAbortError.js';
13
+
14
+ /** Response header specifying the next poll delay in seconds. */
15
+ export const QUERY_POLL_AFTER_HEADER = 'Cratis-Query-Poll-After';
16
+
17
+ /** Observes a query using sequential HTTP GET snapshots rather than a persistent connection. */
18
+ export class PollingQueryConnection<TDataType> implements IObservableQueryConnection<TDataType> {
19
+ private _started = false;
20
+ private _disconnected = false;
21
+ private _unauthorized = false;
22
+ private _timer?: ReturnType<typeof setTimeout>;
23
+ private _controller?: AbortController;
24
+ private _failures = 0;
25
+ private _previousComparable?: object;
26
+ private _lastPingLatency = 0;
27
+ private _totalLatency = 0;
28
+ private _requestCount = 0;
29
+
30
+ /**
31
+ * Initializes a polling connection.
32
+ * @param _url The per-query HTTP endpoint.
33
+ * @param _httpHeadersCallback Custom headers evaluated for each request.
34
+ * @param _microservice The microservice identifier sent in the configured HTTP header.
35
+ * @param _options The resolved timing policy for this connection.
36
+ */
37
+ constructor(
38
+ private readonly _url: URL,
39
+ private readonly _httpHeadersCallback: GetHttpHeaders,
40
+ private readonly _microservice: string,
41
+ private readonly _options: PollingOptions
42
+ ) {}
43
+
44
+ /** @inheritdoc */
45
+ get lastPingLatency(): number { return this._lastPingLatency; }
46
+
47
+ /** @inheritdoc */
48
+ get averageLatency(): number {
49
+ return this._requestCount ? this._totalLatency / this._requestCount : 0;
50
+ }
51
+
52
+ /** @inheritdoc */
53
+ connect(dataReceived: DataReceived<TDataType>, queryArguments?: object): void {
54
+ if (this._started || this._disconnected || this._unauthorized) return;
55
+ this._started = true;
56
+ const url = new URL(this._url);
57
+ const parameters = UrlHelpers.buildQueryParams(queryArguments ?? {}, { waitForFirstResult: 'true' });
58
+ parameters.forEach((value, key) => url.searchParams.append(key, value));
59
+ void this.poll(url, dataReceived);
60
+ }
61
+
62
+ /** @inheritdoc */
63
+ disconnect(): void {
64
+ this._disconnected = true;
65
+ clearTimeout(this._timer);
66
+ this._timer = undefined;
67
+ this._controller?.abort();
68
+ }
69
+
70
+ private async poll(url: URL, dataReceived: DataReceived<TDataType>): Promise<void> {
71
+ this._controller = new AbortController();
72
+ const started = performance.now();
73
+ let delay: number;
74
+ try {
75
+ const headers = new Headers({
76
+ ...Object.fromEntries(new Headers(Globals.httpHeadersCallback?.())),
77
+ ...Object.fromEntries(new Headers(this._httpHeadersCallback()))
78
+ });
79
+ headers.set('Accept', 'application/json');
80
+ headers.set('Content-Type', 'application/json');
81
+ if (this._microservice) headers.set(Globals.microserviceHttpHeader, this._microservice);
82
+ const response = await fetch(url.toString(), { method: 'GET', headers, signal: this._controller.signal });
83
+ if (this._disconnected) return;
84
+ const body = await response.text();
85
+ if (this._disconnected) return;
86
+ this._lastPingLatency = performance.now() - started;
87
+ this._totalLatency += this._lastPingLatency;
88
+ this._requestCount++;
89
+ delay = this.handleResponse(response, body, dataReceived);
90
+ } catch (error) {
91
+ if (this._disconnected || this._unauthorized || isAbortError(error)) return;
92
+ delay = this.backoff();
93
+ }
94
+ if (!this._disconnected && !this._unauthorized) {
95
+ this._timer = setTimeout(() => { void this.poll(url, dataReceived); }, delay);
96
+ }
97
+ }
98
+
99
+ private handleResponse(response: Response, body: string, dataReceived: DataReceived<TDataType>): number {
100
+ let result = this.parseResult(body);
101
+ if (response.status === 401 || response.status === 403 || result?.isAuthorized === false) {
102
+ console.warn(`Polling: query for '${this._url}' was ended by the server - no longer authorized`);
103
+ this._unauthorized = true;
104
+ result ??= { ...QueryResult.unauthorized<TDataType>() } as QueryResult<TDataType>;
105
+ result = { ...result, isAuthorized: false } as QueryResult<TDataType>;
106
+ this.emitChanged(result, JSON.stringify(result), dataReceived);
107
+ return 0;
108
+ }
109
+ if (result?.isValid === false) {
110
+ this.disconnect();
111
+ this.emitChanged(result, body, dataReceived);
112
+ return 0;
113
+ }
114
+ const success = response.status === 200 && result !== undefined;
115
+ if (result && (success || response.status >= 500)) this.emitChanged(result, body, dataReceived);
116
+ if (success) this._failures = 0;
117
+ const delay = success ? this.clamp(this._options.intervalMs) : this.backoff();
118
+ const hint = response.headers.get(QUERY_POLL_AFTER_HEADER);
119
+ const seconds = hint?.trim() ? Number(hint) : NaN;
120
+ return Number.isFinite(seconds) && seconds >= 0 ? this.clamp(seconds * 1000) : delay;
121
+ }
122
+
123
+ private parseResult(body: string): QueryResult<TDataType> | undefined {
124
+ try {
125
+ const result = JSON.parse(body) as QueryResult<TDataType>;
126
+ if (result && typeof result === 'object' && typeof result.isAuthorized === 'boolean') return result;
127
+ } catch {
128
+ // Non-JSON responses are transient failures, except for terminal authentication failures.
129
+ }
130
+ return undefined;
131
+ }
132
+
133
+ private emitChanged(result: QueryResult<TDataType>, body: string, dataReceived: DataReceived<TDataType>): void {
134
+ const comparable = JSON.parse(body) as { correlationId?: string };
135
+ delete comparable.correlationId;
136
+ if (deepEqual(this._previousComparable, comparable)) return;
137
+ this._previousComparable = comparable;
138
+ dataReceived(result);
139
+ }
140
+
141
+ private backoff(): number {
142
+ const jitter = 1 + this._options.jitterRatio * (2 * Math.random() - 1);
143
+ return this.clamp(this._options.intervalMs * this._options.backoffMultiplier ** ++this._failures * jitter);
144
+ }
145
+
146
+ private clamp(delay: number): number {
147
+ return Math.min(this._options.maxIntervalMs, Math.max(this._options.minIntervalMs, delay));
148
+ }
149
+ }
@@ -14,4 +14,7 @@ export enum QueryTransportMethod {
14
14
  * Use Server-Sent Events (SSE) for real-time data streaming.
15
15
  */
16
16
  ServerSentEvents = 'serverSentEvents',
17
+
18
+ /** Use HTTP GET snapshots for observable queries. */
19
+ Polling = 'polling',
17
20
  }
@@ -0,0 +1,47 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import sinon from 'sinon';
5
+ import { given } from '../../../given.js';
6
+ import { Globals } from '../../../Globals.js';
7
+ import { a_descriptor } from '../given/a_descriptor.js';
8
+ import { QueryTransportMethod } from '../../QueryTransportMethod.js';
9
+ import { PollingQueryConnection } from '../../PollingQueryConnection.js';
10
+ import { createObservableQueryConnection } from '../../ObservableQueryConnectionFactory.js';
11
+ import { getSharedMultiplexer, resetSharedMultiplexer } from '../../ObservableQueryMultiplexer.js';
12
+ import { IObservableQueryConnection } from '../../IObservableQueryConnection.js';
13
+
14
+ describe('when creating with direct mode and polling transport', given(a_descriptor, context => {
15
+ let connection: IObservableQueryConnection<unknown>;
16
+ let eventSource: sinon.SinonStub;
17
+ let webSocket: sinon.SinonStub;
18
+ const originalEventSource = Object.getOwnPropertyDescriptor(globalThis, 'EventSource');
19
+ const originalWebSocket = Object.getOwnPropertyDescriptor(globalThis, 'WebSocket');
20
+
21
+ beforeEach(() => {
22
+ resetSharedMultiplexer();
23
+ Globals.queryDirectMode = true;
24
+ Globals.queryTransportMethod = QueryTransportMethod.Polling;
25
+ eventSource = sinon.stub().throws('EventSource must not be used');
26
+ webSocket = sinon.stub().throws('WebSocket must not be used');
27
+ Object.defineProperty(globalThis, 'EventSource', { configurable: true, value: eventSource });
28
+ Object.defineProperty(globalThis, 'WebSocket', { configurable: true, value: webSocket });
29
+ connection = createObservableQueryConnection(context.descriptor);
30
+ });
31
+ afterEach(() => {
32
+ connection?.disconnect();
33
+ Globals.queryDirectMode = context.originalDirectMode;
34
+ Globals.queryTransportMethod = context.originalTransportMethod;
35
+ if (originalEventSource) Object.defineProperty(globalThis, 'EventSource', originalEventSource);
36
+ else delete (globalThis as Record<string, unknown>).EventSource;
37
+ if (originalWebSocket) Object.defineProperty(globalThis, 'WebSocket', originalWebSocket);
38
+ else delete (globalThis as Record<string, unknown>).WebSocket;
39
+ resetSharedMultiplexer();
40
+ });
41
+ it('should return a polling connection', () => connection.should.be.instanceOf(PollingQueryConnection));
42
+ it('should not create persistent transports', () => {
43
+ eventSource.called.should.be.false;
44
+ webSocket.called.should.be.false;
45
+ });
46
+ it('should not create a multiplexer', () => (getSharedMultiplexer() === undefined).should.be.true);
47
+ }));
@@ -0,0 +1,47 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import sinon from 'sinon';
5
+ import { given } from '../../../given.js';
6
+ import { Globals } from '../../../Globals.js';
7
+ import { a_descriptor } from '../given/a_descriptor.js';
8
+ import { QueryTransportMethod } from '../../QueryTransportMethod.js';
9
+ import { PollingQueryConnection } from '../../PollingQueryConnection.js';
10
+ import { createObservableQueryConnection } from '../../ObservableQueryConnectionFactory.js';
11
+ import { getSharedMultiplexer, resetSharedMultiplexer } from '../../ObservableQueryMultiplexer.js';
12
+ import { IObservableQueryConnection } from '../../IObservableQueryConnection.js';
13
+
14
+ describe('when creating with hub mode and polling transport', given(a_descriptor, context => {
15
+ let connection: IObservableQueryConnection<unknown>;
16
+ let eventSource: sinon.SinonStub;
17
+ let webSocket: sinon.SinonStub;
18
+ const originalEventSource = Object.getOwnPropertyDescriptor(globalThis, 'EventSource');
19
+ const originalWebSocket = Object.getOwnPropertyDescriptor(globalThis, 'WebSocket');
20
+
21
+ beforeEach(() => {
22
+ resetSharedMultiplexer();
23
+ Globals.queryDirectMode = false;
24
+ Globals.queryTransportMethod = QueryTransportMethod.Polling;
25
+ eventSource = sinon.stub().throws('EventSource must not be used');
26
+ webSocket = sinon.stub().throws('WebSocket must not be used');
27
+ Object.defineProperty(globalThis, 'EventSource', { configurable: true, value: eventSource });
28
+ Object.defineProperty(globalThis, 'WebSocket', { configurable: true, value: webSocket });
29
+ connection = createObservableQueryConnection(context.descriptor);
30
+ });
31
+ afterEach(() => {
32
+ connection?.disconnect();
33
+ Globals.queryDirectMode = context.originalDirectMode;
34
+ Globals.queryTransportMethod = context.originalTransportMethod;
35
+ if (originalEventSource) Object.defineProperty(globalThis, 'EventSource', originalEventSource);
36
+ else delete (globalThis as Record<string, unknown>).EventSource;
37
+ if (originalWebSocket) Object.defineProperty(globalThis, 'WebSocket', originalWebSocket);
38
+ else delete (globalThis as Record<string, unknown>).WebSocket;
39
+ resetSharedMultiplexer();
40
+ });
41
+ it('should return a polling connection', () => connection.should.be.instanceOf(PollingQueryConnection));
42
+ it('should not create persistent transports', () => {
43
+ eventSource.called.should.be.false;
44
+ webSocket.called.should.be.false;
45
+ });
46
+ it('should not create a multiplexer', () => (getSharedMultiplexer() === undefined).should.be.true);
47
+ }));
@@ -0,0 +1,115 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import sinon from 'sinon';
5
+ import { given } from '../../../given.js';
6
+ import { Globals } from '../../../Globals.js';
7
+ import { createFetchHelper } from '../../../helpers/fetchHelper.js';
8
+ import { an_observable_query_for } from '../given/an_observable_query_for.js';
9
+ import { QueryTransportMethod } from '../../QueryTransportMethod.js';
10
+ import { ObservableQuerySubscription } from '../../ObservableQuerySubscription.js';
11
+ import { Paging } from '../../Paging.js';
12
+ import { Sorting } from '../../Sorting.js';
13
+ import { SortDirection } from '../../SortDirection.js';
14
+
15
+ describe('when subscribing with polling transport', given(an_observable_query_for, context => {
16
+ const fetchHelper = createFetchHelper();
17
+ let originals: Pick<typeof Globals, 'queryDirectMode' | 'queryTransportMethod' | 'queryPollingOptions'>;
18
+ let subscription: ObservableQuerySubscription<string>;
19
+ let clock: sinon.SinonFakeTimers;
20
+ let fetchStub: sinon.SinonStub;
21
+ let callback: sinon.SinonStub;
22
+ let callsBeforeInterval: number;
23
+ let callsAtInterval: number;
24
+
25
+ beforeEach(async () => {
26
+ originals = { queryDirectMode: Globals.queryDirectMode, queryTransportMethod: Globals.queryTransportMethod, queryPollingOptions: Globals.queryPollingOptions };
27
+ Globals.queryDirectMode = false;
28
+ Globals.queryTransportMethod = QueryTransportMethod.Polling;
29
+ Globals.queryPollingOptions = { ...Globals.queryPollingOptions, minIntervalMs: 100 };
30
+ // Keep Response body microtasks native; faking them stalls Node 23's body reader.
31
+ clock = sinon.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout', 'performance'] });
32
+ fetchStub = fetchHelper.stubFetch();
33
+ fetchStub.callsFake(async () => new Response(JSON.stringify({
34
+ data: 'value', isSuccess: true, isAuthorized: true, isValid: true,
35
+ hasExceptions: false, validationResults: [], exceptionMessages: [], exceptionStackTrace: '',
36
+ paging: { page: 2, size: 10, totalItems: 35, totalPages: 4 }
37
+ })));
38
+ callback = sinon.stub();
39
+ const query = context.queryWithRouteAndQueryArgs;
40
+ query.setOrigin('https://example.com');
41
+ query.setMicroservice('svc');
42
+ query.setHttpHeadersCallback(() => ({ Authorization: 'Bearer test' }));
43
+ query.setPollingOptions({ intervalMs: 250 });
44
+ query.paging = new Paging(2, 10);
45
+ query.sorting = new Sorting('name', SortDirection.descending);
46
+ subscription = query.subscribe(callback, { id: 'item-42', filter: 'active', limit: 50 });
47
+ await clock.tickAsync(0);
48
+ await clock.tickAsync(249);
49
+ callsBeforeInterval = fetchStub.callCount;
50
+ await clock.tickAsync(1);
51
+ callsAtInterval = fetchStub.callCount;
52
+ subscription.unsubscribe();
53
+ await clock.tickAsync(2000);
54
+ });
55
+ afterEach(() => {
56
+ subscription?.unsubscribe();
57
+ clock.restore();
58
+ fetchHelper.restore();
59
+ Object.assign(Globals, originals);
60
+ });
61
+ it('should embed route arguments and append only HTTP query arguments', () => {
62
+ const url = new URL(fetchStub.firstCall.args[0]);
63
+ url.pathname.should.equal('/api/items/item-42');
64
+ Object.fromEntries(url.searchParams).should.deep.equal({ filter: 'active', limit: '50', page: '2', pageSize: '10', sortBy: 'name', sortDirection: 'desc', waitForFirstResult: 'true' });
65
+ });
66
+ it('should use per-query headers', () => fetchStub.firstCall.args[1].headers.get('Authorization').should.equal('Bearer test'));
67
+ it('should deliver a ready deserialized result with paging metadata', () => {
68
+ callback.firstCall.args[0].data.should.equal('value');
69
+ callback.firstCall.args[0].isReady.should.be.true;
70
+ callback.firstCall.args[0].paging.totalItems.should.equal(35);
71
+ });
72
+ it('should apply the per-query polling interval', () => {
73
+ callsBeforeInterval.should.equal(1);
74
+ callsAtInterval.should.equal(2);
75
+ });
76
+ it('should suppress unchanged snapshots after deserialization', () => callback.callCount.should.equal(1));
77
+ it('should stop after unsubscribe', () => fetchStub.callCount.should.equal(callsAtInterval));
78
+ }));
79
+
80
+ describe('when subscribing with polling transport and invalid arguments', given(an_observable_query_for, context => {
81
+ const fetchHelper = createFetchHelper();
82
+ let originalTransport: QueryTransportMethod;
83
+ let subscription: ObservableQuerySubscription<string>;
84
+ let clock: sinon.SinonFakeTimers;
85
+ let fetchStub: sinon.SinonStub;
86
+ let callback: sinon.SinonStub;
87
+ beforeEach(async () => {
88
+ originalTransport = Globals.queryTransportMethod;
89
+ Globals.queryTransportMethod = QueryTransportMethod.Polling;
90
+ clock = sinon.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout', 'performance'] });
91
+ fetchStub = fetchHelper.stubFetch();
92
+ fetchStub.callsFake(async () => new Response(JSON.stringify({
93
+ data: '', isSuccess: false, isAuthorized: true, isValid: false,
94
+ hasExceptions: false, validationResults: [{ message: 'Invalid filter' }],
95
+ exceptionMessages: [], exceptionStackTrace: '',
96
+ paging: { page: 0, size: 0, totalItems: 0, totalPages: 0 }
97
+ }), { status: 400 }));
98
+ callback = sinon.stub();
99
+ context.queryWithRouteAndQueryArgs.setOrigin('https://example.com');
100
+ subscription = context.queryWithRouteAndQueryArgs.subscribe(callback, { id: 'item', filter: 'invalid', limit: 10 });
101
+ await clock.tickAsync(60000);
102
+ });
103
+ afterEach(() => {
104
+ subscription?.unsubscribe();
105
+ clock.restore();
106
+ fetchHelper.restore();
107
+ Globals.queryTransportMethod = originalTransport;
108
+ });
109
+ it('should deliver ready validation results', () => {
110
+ callback.firstCall.args[0].isReady.should.be.true;
111
+ callback.firstCall.args[0].isValid.should.be.false;
112
+ callback.firstCall.args[0].validationResults[0].message.should.equal('Invalid filter');
113
+ });
114
+ it('should stop polling', () => fetchStub.callCount.should.equal(1));
115
+ }));
@@ -0,0 +1,44 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import sinon from 'sinon';
5
+ import { given } from '../../../given.js';
6
+ import { Globals } from '../../../Globals.js';
7
+ import { createFetchHelper } from '../../../helpers/fetchHelper.js';
8
+ import { an_observable_query_for } from '../given/an_observable_query_for.js';
9
+ import { QueryTransportMethod } from '../../QueryTransportMethod.js';
10
+ import { ObservableQuerySubscription } from '../../ObservableQuerySubscription.js';
11
+
12
+ describe('when subscribing with polling transport and unauthorized response', given(an_observable_query_for, context => {
13
+ const fetchHelper = createFetchHelper();
14
+ let originalTransport: QueryTransportMethod;
15
+ let clock: sinon.SinonFakeTimers;
16
+ let fetchStub: sinon.SinonStub;
17
+ let callback: sinon.SinonStub;
18
+ let subscription: ObservableQuerySubscription<string>;
19
+ beforeEach(async () => {
20
+ originalTransport = Globals.queryTransportMethod;
21
+ Globals.queryTransportMethod = QueryTransportMethod.Polling;
22
+ // Keep Response body microtasks native; faking them stalls Node 23's body reader.
23
+ clock = sinon.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout', 'performance'] });
24
+ sinon.stub(console, 'warn');
25
+ fetchStub = fetchHelper.stubFetch();
26
+ fetchStub.callsFake(async () => new Response('Unauthorized', { status: 401 }));
27
+ callback = sinon.stub();
28
+ context.queryWithRouteAndQueryArgs.setOrigin('https://example.com');
29
+ subscription = context.queryWithRouteAndQueryArgs.subscribe(callback, { id: 'item', filter: 'active', limit: 10 });
30
+ await clock.tickAsync(60000);
31
+ });
32
+ afterEach(() => {
33
+ subscription?.unsubscribe();
34
+ clock.restore();
35
+ fetchHelper.restore();
36
+ sinon.restore();
37
+ Globals.queryTransportMethod = originalTransport;
38
+ });
39
+ it('should deliver a ready unauthorized result', () => {
40
+ callback.firstCall.args[0].isAuthorized.should.be.false;
41
+ callback.firstCall.args[0].isReady.should.be.true;
42
+ });
43
+ it('should stop polling', () => fetchStub.callCount.should.equal(1));
44
+ }));
@@ -0,0 +1,60 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import sinon from 'sinon';
5
+ import { Globals } from '../../../Globals.js';
6
+ import { createFetchHelper } from '../../../helpers/fetchHelper.js';
7
+ import { PollingQueryConnection } from '../../PollingQueryConnection.js';
8
+
9
+ export class a_polling_query_connection {
10
+ connection!: PollingQueryConnection<unknown>;
11
+ dataReceived!: sinon.SinonStub;
12
+ fetchStub!: sinon.SinonStub;
13
+ clock!: sinon.SinonFakeTimers;
14
+ random!: sinon.SinonStub;
15
+ responses: Response[] = [];
16
+ private _correlationId = 0;
17
+
18
+ constructor() {
19
+ const fetchHelper = createFetchHelper();
20
+ const originalOptions = Globals.queryPollingOptions;
21
+ beforeEach(() => {
22
+ // Keep Response body microtasks native; faking them stalls Node 23's body reader.
23
+ this.clock = sinon.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout', 'performance'] });
24
+ this.random = sinon.stub(Math, 'random').returns(0.5);
25
+ sinon.stub(console, 'warn');
26
+ this.responses = [
27
+ new Response(JSON.stringify(this.envelope('same'))),
28
+ new Response(JSON.stringify(this.envelope('same')))
29
+ ];
30
+ this.fetchStub = fetchHelper.stubFetch();
31
+ this.fetchStub.callsFake(async () => this.responses.shift() ?? new Response(JSON.stringify(this.envelope('same'))));
32
+ this.dataReceived = sinon.stub();
33
+ this.connection = new PollingQueryConnection(new URL('https://example.com/api/items/42'),
34
+ () => ({ Authorization: 'Bearer x' }), 'svc', {
35
+ intervalMs: 1000, minIntervalMs: 500, maxIntervalMs: 8000,
36
+ backoffMultiplier: 2, jitterRatio: 0.2
37
+ });
38
+ });
39
+ afterEach(() => {
40
+ this.connection.disconnect();
41
+ this.clock.restore();
42
+ fetchHelper.restore();
43
+ sinon.restore();
44
+ Globals.queryPollingOptions = originalOptions;
45
+ });
46
+ }
47
+
48
+ envelope(data: unknown, overrides: Record<string, unknown> = {}) {
49
+ return {
50
+ data, correlationId: String(++this._correlationId), isSuccess: true,
51
+ isAuthorized: true, isValid: true, hasExceptions: false,
52
+ validationResults: [], exceptionMessages: [], exceptionStackTrace: '',
53
+ paging: { page: 1, size: 10, totalItems: 25, totalPages: 3 }, ...overrides
54
+ };
55
+ }
56
+
57
+ respond(status: number, headers: Record<string, string> = {}, overrides: Record<string, unknown> = {}) {
58
+ this.fetchStub.onFirstCall().resolves(new Response(JSON.stringify(this.envelope('same', overrides)), { status, headers }));
59
+ }
60
+ }
@@ -0,0 +1,19 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { given } from '../../../given.js';
5
+ import { a_polling_query_connection } from '../given/a_polling_query_connection.js';
6
+
7
+ describe('when backing off after a network error', given(a_polling_query_connection, context => {
8
+ let beforeRetry: number;
9
+ beforeEach(async () => {
10
+ context.fetchStub.onFirstCall().rejects(new TypeError('offline'));
11
+ context.connection.connect(context.dataReceived);
12
+ await context.clock.tickAsync(1999);
13
+ beforeRetry = context.fetchStub.callCount;
14
+ await context.clock.tickAsync(1001);
15
+ });
16
+ it('should wait twice the interval before retrying', () => beforeRetry.should.equal(1));
17
+ it('should reset to the normal interval after recovery', () => context.fetchStub.callCount.should.equal(3));
18
+ it('should emit the recovered result', () => context.dataReceived.callCount.should.equal(1));
19
+ }));