@honua/react 0.0.20-alpha.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 (597) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +9 -0
  3. package/contract/analytics-sources.d.ts +149 -0
  4. package/contract/analytics-sources.d.ts.map +1 -0
  5. package/contract/analytics-sources.js +235 -0
  6. package/contract/edit-session.d.ts +203 -0
  7. package/contract/edit-session.d.ts.map +1 -0
  8. package/contract/edit-session.js +760 -0
  9. package/contract/edit-sketch.d.ts +91 -0
  10. package/contract/edit-sketch.d.ts.map +1 -0
  11. package/contract/edit-sketch.js +281 -0
  12. package/contract/index.d.ts +61 -0
  13. package/contract/index.d.ts.map +1 -0
  14. package/contract/index.js +51 -0
  15. package/contract/jobs.d.ts +149 -0
  16. package/contract/jobs.d.ts.map +1 -0
  17. package/contract/jobs.js +40 -0
  18. package/contract/source.d.ts +423 -0
  19. package/contract/source.d.ts.map +1 -0
  20. package/contract/source.js +3802 -0
  21. package/contract/spatial-aggregation.d.ts +336 -0
  22. package/contract/spatial-aggregation.d.ts.map +1 -0
  23. package/contract/spatial-aggregation.js +298 -0
  24. package/contract/temporal.d.ts +455 -0
  25. package/contract/temporal.d.ts.map +1 -0
  26. package/contract/temporal.js +146 -0
  27. package/contract/tiles.d.ts +370 -0
  28. package/contract/tiles.d.ts.map +1 -0
  29. package/contract/tiles.js +519 -0
  30. package/contract/types.d.ts +665 -0
  31. package/contract/types.d.ts.map +1 -0
  32. package/contract/types.js +175 -0
  33. package/contract/widget-source.d.ts +205 -0
  34. package/contract/widget-source.d.ts.map +1 -0
  35. package/contract/widget-source.js +1473 -0
  36. package/control-plane/client.d.ts +95 -0
  37. package/control-plane/client.d.ts.map +1 -0
  38. package/control-plane/client.js +374 -0
  39. package/control-plane/index.d.ts +18 -0
  40. package/control-plane/index.d.ts.map +1 -0
  41. package/control-plane/index.js +14 -0
  42. package/control-plane/types.d.ts +233 -0
  43. package/control-plane/types.d.ts.map +1 -0
  44. package/control-plane/types.js +7 -0
  45. package/core/batch.d.ts +37 -0
  46. package/core/batch.d.ts.map +1 -0
  47. package/core/batch.js +79 -0
  48. package/core/cache-state.d.ts +79 -0
  49. package/core/cache-state.d.ts.map +1 -0
  50. package/core/cache-state.js +87 -0
  51. package/core/client.d.ts +577 -0
  52. package/core/client.d.ts.map +1 -0
  53. package/core/client.js +1664 -0
  54. package/core/compatibility.d.ts +18 -0
  55. package/core/compatibility.d.ts.map +1 -0
  56. package/core/compatibility.js +245 -0
  57. package/core/errors.d.ts +107 -0
  58. package/core/errors.d.ts.map +1 -0
  59. package/core/errors.js +156 -0
  60. package/core/esri-geojson.d.ts +66 -0
  61. package/core/esri-geojson.d.ts.map +1 -0
  62. package/core/esri-geojson.js +363 -0
  63. package/core/geoservices.d.ts +34 -0
  64. package/core/geoservices.d.ts.map +1 -0
  65. package/core/geoservices.js +434 -0
  66. package/core/grpc-adapter.d.ts +23 -0
  67. package/core/grpc-adapter.d.ts.map +1 -0
  68. package/core/grpc-adapter.js +688 -0
  69. package/core/odata.d.ts +347 -0
  70. package/core/odata.d.ts.map +1 -0
  71. package/core/odata.js +913 -0
  72. package/core/ogc-conformance.d.ts +35 -0
  73. package/core/ogc-conformance.d.ts.map +1 -0
  74. package/core/ogc-conformance.js +134 -0
  75. package/core/ogc-features.d.ts +23 -0
  76. package/core/ogc-features.d.ts.map +1 -0
  77. package/core/ogc-features.js +114 -0
  78. package/core/ogc-maps.d.ts +49 -0
  79. package/core/ogc-maps.d.ts.map +1 -0
  80. package/core/ogc-maps.js +126 -0
  81. package/core/ogc-processes.d.ts +118 -0
  82. package/core/ogc-processes.d.ts.map +1 -0
  83. package/core/ogc-processes.js +423 -0
  84. package/core/ogc-records.d.ts +75 -0
  85. package/core/ogc-records.d.ts.map +1 -0
  86. package/core/ogc-records.js +251 -0
  87. package/core/ogc-tiles.d.ts +125 -0
  88. package/core/ogc-tiles.d.ts.map +1 -0
  89. package/core/ogc-tiles.js +183 -0
  90. package/core/path-utils.d.ts +69 -0
  91. package/core/path-utils.d.ts.map +1 -0
  92. package/core/path-utils.js +123 -0
  93. package/core/pbf-decoder.d.ts +39 -0
  94. package/core/pbf-decoder.d.ts.map +1 -0
  95. package/core/pbf-decoder.js +702 -0
  96. package/core/process-runner.d.ts +104 -0
  97. package/core/process-runner.d.ts.map +1 -0
  98. package/core/process-runner.js +315 -0
  99. package/core/protocol-transport.d.ts +76 -0
  100. package/core/protocol-transport.d.ts.map +1 -0
  101. package/core/protocol-transport.js +15 -0
  102. package/core/query-builder.d.ts +261 -0
  103. package/core/query-builder.d.ts.map +1 -0
  104. package/core/query-builder.js +538 -0
  105. package/core/request-pipeline.d.ts +88 -0
  106. package/core/request-pipeline.d.ts.map +1 -0
  107. package/core/request-pipeline.js +273 -0
  108. package/core/spatial-filter.d.ts +93 -0
  109. package/core/spatial-filter.d.ts.map +1 -0
  110. package/core/spatial-filter.js +153 -0
  111. package/core/stac.d.ts +55 -0
  112. package/core/stac.d.ts.map +1 -0
  113. package/core/stac.js +229 -0
  114. package/core/surfaces.d.ts +527 -0
  115. package/core/surfaces.d.ts.map +1 -0
  116. package/core/surfaces.js +1917 -0
  117. package/core/terrain-elevation.d.ts +114 -0
  118. package/core/terrain-elevation.d.ts.map +1 -0
  119. package/core/terrain-elevation.js +150 -0
  120. package/core/types.d.ts +1355 -0
  121. package/core/types.d.ts.map +1 -0
  122. package/core/types.js +13 -0
  123. package/core/wfs-capabilities.d.ts +143 -0
  124. package/core/wfs-capabilities.d.ts.map +1 -0
  125. package/core/wfs-capabilities.js +478 -0
  126. package/core/wfs-filter.d.ts +118 -0
  127. package/core/wfs-filter.d.ts.map +1 -0
  128. package/core/wfs-filter.js +634 -0
  129. package/core/wfs.d.ts +219 -0
  130. package/core/wfs.d.ts.map +1 -0
  131. package/core/wfs.js +406 -0
  132. package/core/wire-shared.d.ts +32 -0
  133. package/core/wire-shared.d.ts.map +1 -0
  134. package/core/wire-shared.js +76 -0
  135. package/core/wms-axis.d.ts +36 -0
  136. package/core/wms-axis.d.ts.map +1 -0
  137. package/core/wms-axis.js +73 -0
  138. package/core/wms-capabilities.d.ts +110 -0
  139. package/core/wms-capabilities.d.ts.map +1 -0
  140. package/core/wms-capabilities.js +559 -0
  141. package/core/wms-types.d.ts +128 -0
  142. package/core/wms-types.d.ts.map +1 -0
  143. package/core/wms-types.js +32 -0
  144. package/core/wms.d.ts +144 -0
  145. package/core/wms.d.ts.map +1 -0
  146. package/core/wms.js +338 -0
  147. package/core/wmts-capabilities.d.ts +82 -0
  148. package/core/wmts-capabilities.d.ts.map +1 -0
  149. package/core/wmts-capabilities.js +402 -0
  150. package/core/wmts.d.ts +103 -0
  151. package/core/wmts.d.ts.map +1 -0
  152. package/core/wmts.js +332 -0
  153. package/core/xml-text.d.ts +20 -0
  154. package/core/xml-text.d.ts.map +1 -0
  155. package/core/xml-text.js +85 -0
  156. package/esri-compat/basemap-gallery.d.ts +38 -0
  157. package/esri-compat/basemap-gallery.d.ts.map +1 -0
  158. package/esri-compat/basemap-gallery.js +159 -0
  159. package/esri-compat/basemap-layer-list.d.ts +35 -0
  160. package/esri-compat/basemap-layer-list.d.ts.map +1 -0
  161. package/esri-compat/basemap-layer-list.js +139 -0
  162. package/esri-compat/basemap.d.ts +32 -0
  163. package/esri-compat/basemap.d.ts.map +1 -0
  164. package/esri-compat/basemap.js +84 -0
  165. package/esri-compat/bookmarks.d.ts +36 -0
  166. package/esri-compat/bookmarks.d.ts.map +1 -0
  167. package/esri-compat/bookmarks.js +108 -0
  168. package/esri-compat/class-breaks-renderer.d.ts +50 -0
  169. package/esri-compat/class-breaks-renderer.d.ts.map +1 -0
  170. package/esri-compat/class-breaks-renderer.js +164 -0
  171. package/esri-compat/color.d.ts +22 -0
  172. package/esri-compat/color.d.ts.map +1 -0
  173. package/esri-compat/color.js +106 -0
  174. package/esri-compat/controls.d.ts +174 -0
  175. package/esri-compat/controls.d.ts.map +1 -0
  176. package/esri-compat/controls.js +479 -0
  177. package/esri-compat/coordinate-conversion.d.ts +41 -0
  178. package/esri-compat/coordinate-conversion.d.ts.map +1 -0
  179. package/esri-compat/coordinate-conversion.js +125 -0
  180. package/esri-compat/directions.d.ts +45 -0
  181. package/esri-compat/directions.d.ts.map +1 -0
  182. package/esri-compat/directions.js +123 -0
  183. package/esri-compat/editor.d.ts +47 -0
  184. package/esri-compat/editor.d.ts.map +1 -0
  185. package/esri-compat/editor.js +170 -0
  186. package/esri-compat/esri-config.d.ts +16 -0
  187. package/esri-compat/esri-config.d.ts.map +1 -0
  188. package/esri-compat/esri-config.js +23 -0
  189. package/esri-compat/esri-request.d.ts +17 -0
  190. package/esri-compat/esri-request.d.ts.map +1 -0
  191. package/esri-compat/esri-request.js +127 -0
  192. package/esri-compat/event-bus.d.ts +106 -0
  193. package/esri-compat/event-bus.d.ts.map +1 -0
  194. package/esri-compat/event-bus.js +90 -0
  195. package/esri-compat/expand.d.ts +36 -0
  196. package/esri-compat/expand.d.ts.map +1 -0
  197. package/esri-compat/expand.js +97 -0
  198. package/esri-compat/extent.d.ts +43 -0
  199. package/esri-compat/extent.d.ts.map +1 -0
  200. package/esri-compat/extent.js +140 -0
  201. package/esri-compat/feature-filter.d.ts +55 -0
  202. package/esri-compat/feature-filter.d.ts.map +1 -0
  203. package/esri-compat/feature-filter.js +109 -0
  204. package/esri-compat/feature-form.d.ts +64 -0
  205. package/esri-compat/feature-form.d.ts.map +1 -0
  206. package/esri-compat/feature-form.js +137 -0
  207. package/esri-compat/feature-layer.d.ts +173 -0
  208. package/esri-compat/feature-layer.d.ts.map +1 -0
  209. package/esri-compat/feature-layer.js +661 -0
  210. package/esri-compat/feature-set.d.ts +32 -0
  211. package/esri-compat/feature-set.d.ts.map +1 -0
  212. package/esri-compat/feature-set.js +80 -0
  213. package/esri-compat/feature-table.d.ts +141 -0
  214. package/esri-compat/feature-table.d.ts.map +1 -0
  215. package/esri-compat/feature-table.js +500 -0
  216. package/esri-compat/feature-templates.d.ts +41 -0
  217. package/esri-compat/feature-templates.d.ts.map +1 -0
  218. package/esri-compat/feature-templates.js +97 -0
  219. package/esri-compat/feature.d.ts +33 -0
  220. package/esri-compat/feature.d.ts.map +1 -0
  221. package/esri-compat/feature.js +83 -0
  222. package/esri-compat/geojson-layer.d.ts +86 -0
  223. package/esri-compat/geojson-layer.d.ts.map +1 -0
  224. package/esri-compat/geojson-layer.js +191 -0
  225. package/esri-compat/geometry-engine.d.ts +107 -0
  226. package/esri-compat/geometry-engine.d.ts.map +1 -0
  227. package/esri-compat/geometry-engine.js +266 -0
  228. package/esri-compat/graphic.d.ts +46 -0
  229. package/esri-compat/graphic.d.ts.map +1 -0
  230. package/esri-compat/graphic.js +103 -0
  231. package/esri-compat/graphics-layer.d.ts +49 -0
  232. package/esri-compat/graphics-layer.d.ts.map +1 -0
  233. package/esri-compat/graphics-layer.js +185 -0
  234. package/esri-compat/group-layer.d.ts +48 -0
  235. package/esri-compat/group-layer.d.ts.map +1 -0
  236. package/esri-compat/group-layer.js +212 -0
  237. package/esri-compat/identify.d.ts +71 -0
  238. package/esri-compat/identify.d.ts.map +1 -0
  239. package/esri-compat/identify.js +299 -0
  240. package/esri-compat/identity-manager.d.ts +31 -0
  241. package/esri-compat/identity-manager.d.ts.map +1 -0
  242. package/esri-compat/identity-manager.js +115 -0
  243. package/esri-compat/imagery-layer.d.ts +78 -0
  244. package/esri-compat/imagery-layer.d.ts.map +1 -0
  245. package/esri-compat/imagery-layer.js +167 -0
  246. package/esri-compat/label-class.d.ts +31 -0
  247. package/esri-compat/label-class.d.ts.map +1 -0
  248. package/esri-compat/label-class.js +102 -0
  249. package/esri-compat/layer-list.d.ts +77 -0
  250. package/esri-compat/layer-list.d.ts.map +1 -0
  251. package/esri-compat/layer-list.js +342 -0
  252. package/esri-compat/legend.d.ts +53 -0
  253. package/esri-compat/legend.d.ts.map +1 -0
  254. package/esri-compat/legend.js +254 -0
  255. package/esri-compat/map-image-layer.d.ts +162 -0
  256. package/esri-compat/map-image-layer.d.ts.map +1 -0
  257. package/esri-compat/map-image-layer.js +709 -0
  258. package/esri-compat/map-view.d.ts +323 -0
  259. package/esri-compat/map-view.d.ts.map +1 -0
  260. package/esri-compat/map-view.js +1175 -0
  261. package/esri-compat/map.d.ts +47 -0
  262. package/esri-compat/map.d.ts.map +1 -0
  263. package/esri-compat/map.js +209 -0
  264. package/esri-compat/measurement-2d.d.ts +61 -0
  265. package/esri-compat/measurement-2d.d.ts.map +1 -0
  266. package/esri-compat/measurement-2d.js +184 -0
  267. package/esri-compat/measurement.d.ts +45 -0
  268. package/esri-compat/measurement.d.ts.map +1 -0
  269. package/esri-compat/measurement.js +193 -0
  270. package/esri-compat/oauth-info.d.ts +35 -0
  271. package/esri-compat/oauth-info.d.ts.map +1 -0
  272. package/esri-compat/oauth-info.js +115 -0
  273. package/esri-compat/picture-marker-symbol.d.ts +35 -0
  274. package/esri-compat/picture-marker-symbol.d.ts.map +1 -0
  275. package/esri-compat/picture-marker-symbol.js +126 -0
  276. package/esri-compat/point.d.ts +31 -0
  277. package/esri-compat/point.d.ts.map +1 -0
  278. package/esri-compat/point.js +102 -0
  279. package/esri-compat/polygon.d.ts +31 -0
  280. package/esri-compat/polygon.d.ts.map +1 -0
  281. package/esri-compat/polygon.js +107 -0
  282. package/esri-compat/polyline.d.ts +31 -0
  283. package/esri-compat/polyline.d.ts.map +1 -0
  284. package/esri-compat/polyline.js +107 -0
  285. package/esri-compat/popup-template.d.ts +55 -0
  286. package/esri-compat/popup-template.d.ts.map +1 -0
  287. package/esri-compat/popup-template.js +128 -0
  288. package/esri-compat/popup.d.ts +54 -0
  289. package/esri-compat/popup.d.ts.map +1 -0
  290. package/esri-compat/popup.js +312 -0
  291. package/esri-compat/print.d.ts +47 -0
  292. package/esri-compat/print.d.ts.map +1 -0
  293. package/esri-compat/print.js +105 -0
  294. package/esri-compat/query.d.ts +58 -0
  295. package/esri-compat/query.d.ts.map +1 -0
  296. package/esri-compat/query.js +128 -0
  297. package/esri-compat/reactive-utils.d.ts +24 -0
  298. package/esri-compat/reactive-utils.d.ts.map +1 -0
  299. package/esri-compat/reactive-utils.js +193 -0
  300. package/esri-compat/request.d.ts +41 -0
  301. package/esri-compat/request.d.ts.map +1 -0
  302. package/esri-compat/request.js +253 -0
  303. package/esri-compat/route-layer.d.ts +59 -0
  304. package/esri-compat/route-layer.d.ts.map +1 -0
  305. package/esri-compat/route-layer.js +223 -0
  306. package/esri-compat/route-task.d.ts +88 -0
  307. package/esri-compat/route-task.d.ts.map +1 -0
  308. package/esri-compat/route-task.js +225 -0
  309. package/esri-compat/scene-view.d.ts +41 -0
  310. package/esri-compat/scene-view.d.ts.map +1 -0
  311. package/esri-compat/scene-view.js +68 -0
  312. package/esri-compat/search.d.ts +112 -0
  313. package/esri-compat/search.d.ts.map +1 -0
  314. package/esri-compat/search.js +820 -0
  315. package/esri-compat/simple-fill-symbol.d.ts +27 -0
  316. package/esri-compat/simple-fill-symbol.d.ts.map +1 -0
  317. package/esri-compat/simple-fill-symbol.js +85 -0
  318. package/esri-compat/simple-line-symbol.d.ts +27 -0
  319. package/esri-compat/simple-line-symbol.d.ts.map +1 -0
  320. package/esri-compat/simple-line-symbol.js +85 -0
  321. package/esri-compat/simple-marker-symbol.d.ts +29 -0
  322. package/esri-compat/simple-marker-symbol.d.ts.map +1 -0
  323. package/esri-compat/simple-marker-symbol.js +92 -0
  324. package/esri-compat/simple-renderer.d.ts +29 -0
  325. package/esri-compat/simple-renderer.d.ts.map +1 -0
  326. package/esri-compat/simple-renderer.js +92 -0
  327. package/esri-compat/sketch.d.ts +64 -0
  328. package/esri-compat/sketch.d.ts.map +1 -0
  329. package/esri-compat/sketch.js +227 -0
  330. package/esri-compat/spatial-reference.d.ts +31 -0
  331. package/esri-compat/spatial-reference.d.ts.map +1 -0
  332. package/esri-compat/spatial-reference.js +102 -0
  333. package/esri-compat/swipe.d.ts +34 -0
  334. package/esri-compat/swipe.d.ts.map +1 -0
  335. package/esri-compat/swipe.js +93 -0
  336. package/esri-compat/table-list.d.ts +36 -0
  337. package/esri-compat/table-list.d.ts.map +1 -0
  338. package/esri-compat/table-list.js +126 -0
  339. package/esri-compat/text-symbol.d.ts +37 -0
  340. package/esri-compat/text-symbol.d.ts.map +1 -0
  341. package/esri-compat/text-symbol.js +123 -0
  342. package/esri-compat/tile-layer.d.ts +52 -0
  343. package/esri-compat/tile-layer.d.ts.map +1 -0
  344. package/esri-compat/tile-layer.js +168 -0
  345. package/esri-compat/time-slider.d.ts +70 -0
  346. package/esri-compat/time-slider.d.ts.map +1 -0
  347. package/esri-compat/time-slider.js +208 -0
  348. package/esri-compat/track.d.ts +51 -0
  349. package/esri-compat/track.d.ts.map +1 -0
  350. package/esri-compat/track.js +174 -0
  351. package/esri-compat/unique-value-renderer.d.ts +41 -0
  352. package/esri-compat/unique-value-renderer.d.ts.map +1 -0
  353. package/esri-compat/unique-value-renderer.js +127 -0
  354. package/esri-compat/url.d.ts +13 -0
  355. package/esri-compat/url.d.ts.map +1 -0
  356. package/esri-compat/url.js +68 -0
  357. package/esri-compat/vector-tile-layer.d.ts +47 -0
  358. package/esri-compat/vector-tile-layer.d.ts.map +1 -0
  359. package/esri-compat/vector-tile-layer.js +130 -0
  360. package/esri-compat/web-map.d.ts +14 -0
  361. package/esri-compat/web-map.d.ts.map +1 -0
  362. package/esri-compat/web-map.js +24 -0
  363. package/esri-compat/wfs-layer.d.ts +64 -0
  364. package/esri-compat/wfs-layer.d.ts.map +1 -0
  365. package/esri-compat/wfs-layer.js +145 -0
  366. package/esri-compat/wms-layer.d.ts +76 -0
  367. package/esri-compat/wms-layer.d.ts.map +1 -0
  368. package/esri-compat/wms-layer.js +157 -0
  369. package/exploration/context.d.ts +25 -0
  370. package/exploration/context.d.ts.map +1 -0
  371. package/exploration/context.js +396 -0
  372. package/exploration/index.d.ts +27 -0
  373. package/exploration/index.d.ts.map +1 -0
  374. package/exploration/index.js +24 -0
  375. package/exploration/presets.d.ts +27 -0
  376. package/exploration/presets.d.ts.map +1 -0
  377. package/exploration/presets.js +114 -0
  378. package/exploration/reducer.d.ts +23 -0
  379. package/exploration/reducer.d.ts.map +1 -0
  380. package/exploration/reducer.js +332 -0
  381. package/exploration/selection.d.ts +25 -0
  382. package/exploration/selection.d.ts.map +1 -0
  383. package/exploration/selection.js +34 -0
  384. package/exploration/selectors.d.ts +56 -0
  385. package/exploration/selectors.d.ts.map +1 -0
  386. package/exploration/selectors.js +110 -0
  387. package/exploration/types.d.ts +294 -0
  388. package/exploration/types.d.ts.map +1 -0
  389. package/exploration/types.js +33 -0
  390. package/expr/expression.d.ts +457 -0
  391. package/expr/expression.d.ts.map +1 -0
  392. package/expr/expression.js +663 -0
  393. package/expr/index.d.ts +32 -0
  394. package/expr/index.d.ts.map +1 -0
  395. package/expr/index.js +30 -0
  396. package/gen/honua/v1/feature_service_pb.d.ts +918 -0
  397. package/gen/honua/v1/feature_service_pb.d.ts.map +1 -0
  398. package/gen/honua/v1/feature_service_pb.js +375 -0
  399. package/generated-app/errors.d.ts +41 -0
  400. package/generated-app/errors.d.ts.map +1 -0
  401. package/generated-app/errors.js +41 -0
  402. package/generated-app/index.d.ts +17 -0
  403. package/generated-app/index.d.ts.map +1 -0
  404. package/generated-app/index.js +12 -0
  405. package/generated-app/manifest.d.ts +183 -0
  406. package/generated-app/manifest.d.ts.map +1 -0
  407. package/generated-app/manifest.js +14 -0
  408. package/generated-app/projection.d.ts +44 -0
  409. package/generated-app/projection.d.ts.map +1 -0
  410. package/generated-app/projection.js +234 -0
  411. package/generated-app/runtime.d.ts +215 -0
  412. package/generated-app/runtime.d.ts.map +1 -0
  413. package/generated-app/runtime.js +1196 -0
  414. package/interactions/detail-model.d.ts +54 -0
  415. package/interactions/detail-model.d.ts.map +1 -0
  416. package/interactions/detail-model.js +105 -0
  417. package/interactions/exploration-bindings.d.ts +120 -0
  418. package/interactions/exploration-bindings.d.ts.map +1 -0
  419. package/interactions/exploration-bindings.js +218 -0
  420. package/interactions/feature-state.d.ts +144 -0
  421. package/interactions/feature-state.d.ts.map +1 -0
  422. package/interactions/feature-state.js +169 -0
  423. package/interactions/hit-test.d.ts +112 -0
  424. package/interactions/hit-test.d.ts.map +1 -0
  425. package/interactions/hit-test.js +291 -0
  426. package/interactions/index.d.ts +31 -0
  427. package/interactions/index.d.ts.map +1 -0
  428. package/interactions/index.js +24 -0
  429. package/interactions/share-state.d.ts +36 -0
  430. package/interactions/share-state.d.ts.map +1 -0
  431. package/interactions/share-state.js +123 -0
  432. package/map/feature-service-adapter.d.ts +138 -0
  433. package/map/feature-service-adapter.d.ts.map +1 -0
  434. package/map/feature-service-adapter.js +279 -0
  435. package/map/honua-map.d.ts +203 -0
  436. package/map/honua-map.d.ts.map +1 -0
  437. package/map/honua-map.js +347 -0
  438. package/map/index.d.ts +27 -0
  439. package/map/index.d.ts.map +1 -0
  440. package/map/index.js +22 -0
  441. package/map/maplibre-target.d.ts +103 -0
  442. package/map/maplibre-target.d.ts.map +1 -0
  443. package/map/maplibre-target.js +164 -0
  444. package/map/webmap-maplibre.d.ts +60 -0
  445. package/map/webmap-maplibre.d.ts.map +1 -0
  446. package/map/webmap-maplibre.js +212 -0
  447. package/package.json +37 -0
  448. package/react/context.d.ts +32 -0
  449. package/react/context.d.ts.map +1 -0
  450. package/react/context.js +19 -0
  451. package/react/honua-layer.d.ts +35 -0
  452. package/react/honua-layer.d.ts.map +1 -0
  453. package/react/honua-layer.js +47 -0
  454. package/react/honua-map.d.ts +83 -0
  455. package/react/honua-map.d.ts.map +1 -0
  456. package/react/honua-map.js +109 -0
  457. package/react/honua-popup.d.ts +26 -0
  458. package/react/honua-popup.d.ts.map +1 -0
  459. package/react/honua-popup.js +34 -0
  460. package/react/hooks.d.ts +127 -0
  461. package/react/hooks.d.ts.map +1 -0
  462. package/react/hooks.js +206 -0
  463. package/react/index.d.ts +42 -0
  464. package/react/index.d.ts.map +1 -0
  465. package/react/index.js +34 -0
  466. package/react/provider.d.ts +36 -0
  467. package/react/provider.d.ts.map +1 -0
  468. package/react/provider.js +35 -0
  469. package/react/query-cache.d.ts +76 -0
  470. package/react/query-cache.d.ts.map +1 -0
  471. package/react/query-cache.js +188 -0
  472. package/runtime/diff.d.ts +31 -0
  473. package/runtime/diff.d.ts.map +1 -0
  474. package/runtime/diff.js +143 -0
  475. package/runtime/errors.d.ts +40 -0
  476. package/runtime/errors.d.ts.map +1 -0
  477. package/runtime/errors.js +35 -0
  478. package/runtime/index.d.ts +59 -0
  479. package/runtime/index.d.ts.map +1 -0
  480. package/runtime/index.js +43 -0
  481. package/runtime/legend.d.ts +23 -0
  482. package/runtime/legend.d.ts.map +1 -0
  483. package/runtime/legend.js +38 -0
  484. package/runtime/load-package.d.ts +125 -0
  485. package/runtime/load-package.d.ts.map +1 -0
  486. package/runtime/load-package.js +264 -0
  487. package/runtime/map-package-fetch.d.ts +107 -0
  488. package/runtime/map-package-fetch.d.ts.map +1 -0
  489. package/runtime/map-package-fetch.js +336 -0
  490. package/runtime/map-package-realtime.d.ts +169 -0
  491. package/runtime/map-package-realtime.d.ts.map +1 -0
  492. package/runtime/map-package-realtime.js +350 -0
  493. package/runtime/map-package-validation.d.ts +43 -0
  494. package/runtime/map-package-validation.d.ts.map +1 -0
  495. package/runtime/map-package-validation.js +269 -0
  496. package/runtime/map-package-watch.d.ts +78 -0
  497. package/runtime/map-package-watch.d.ts.map +1 -0
  498. package/runtime/map-package-watch.js +695 -0
  499. package/runtime/map-package.d.ts +167 -0
  500. package/runtime/map-package.d.ts.map +1 -0
  501. package/runtime/map-package.js +15 -0
  502. package/runtime/ogc-styles.d.ts +110 -0
  503. package/runtime/ogc-styles.d.ts.map +1 -0
  504. package/runtime/ogc-styles.js +195 -0
  505. package/runtime/popups.d.ts +62 -0
  506. package/runtime/popups.d.ts.map +1 -0
  507. package/runtime/popups.js +95 -0
  508. package/runtime/query-tiles.d.ts +217 -0
  509. package/runtime/query-tiles.d.ts.map +1 -0
  510. package/runtime/query-tiles.js +801 -0
  511. package/runtime/runtime.d.ts +243 -0
  512. package/runtime/runtime.d.ts.map +1 -0
  513. package/runtime/runtime.js +1147 -0
  514. package/runtime/source-bridge.d.ts +99 -0
  515. package/runtime/source-bridge.d.ts.map +1 -0
  516. package/runtime/source-bridge.js +401 -0
  517. package/runtime/style-compose.d.ts +46 -0
  518. package/runtime/style-compose.d.ts.map +1 -0
  519. package/runtime/style-compose.js +144 -0
  520. package/runtime/style-interactions.d.ts +141 -0
  521. package/runtime/style-interactions.d.ts.map +1 -0
  522. package/runtime/style-interactions.js +574 -0
  523. package/runtime/style-spec-validation.d.ts +23 -0
  524. package/runtime/style-spec-validation.d.ts.map +1 -0
  525. package/runtime/style-spec-validation.js +357 -0
  526. package/studio/capability-manifest.d.ts +285 -0
  527. package/studio/capability-manifest.d.ts.map +1 -0
  528. package/studio/capability-manifest.js +59 -0
  529. package/studio/chart-spec.d.ts +85 -0
  530. package/studio/chart-spec.d.ts.map +1 -0
  531. package/studio/chart-spec.js +106 -0
  532. package/studio/index.d.ts +34 -0
  533. package/studio/index.d.ts.map +1 -0
  534. package/studio/index.js +27 -0
  535. package/studio/provenance.d.ts +131 -0
  536. package/studio/provenance.d.ts.map +1 -0
  537. package/studio/provenance.js +39 -0
  538. package/studio/publish.d.ts +64 -0
  539. package/studio/publish.d.ts.map +1 -0
  540. package/studio/publish.js +13 -0
  541. package/studio/types.d.ts +278 -0
  542. package/studio/types.d.ts.map +1 -0
  543. package/studio/types.js +71 -0
  544. package/studio/validate.d.ts +45 -0
  545. package/studio/validate.d.ts.map +1 -0
  546. package/studio/validate.js +255 -0
  547. package/studio/validation.d.ts +112 -0
  548. package/studio/validation.d.ts.map +1 -0
  549. package/studio/validation.js +42 -0
  550. package/style/factory.d.ts +30 -0
  551. package/style/factory.d.ts.map +1 -0
  552. package/style/factory.js +61 -0
  553. package/style/index.d.ts +25 -0
  554. package/style/index.d.ts.map +1 -0
  555. package/style/index.js +24 -0
  556. package/style/specification.d.ts +203 -0
  557. package/style/specification.d.ts.map +1 -0
  558. package/style/specification.js +178 -0
  559. package/style/wms-wmts-resolvers.d.ts +38 -0
  560. package/style/wms-wmts-resolvers.d.ts.map +1 -0
  561. package/style/wms-wmts-resolvers.js +92 -0
  562. package/webmap/contract.d.ts +28 -0
  563. package/webmap/contract.d.ts.map +1 -0
  564. package/webmap/contract.js +130 -0
  565. package/webmap/convert-basemap.d.ts +17 -0
  566. package/webmap/convert-basemap.d.ts.map +1 -0
  567. package/webmap/convert-basemap.js +78 -0
  568. package/webmap/convert-extent.d.ts +17 -0
  569. package/webmap/convert-extent.d.ts.map +1 -0
  570. package/webmap/convert-extent.js +114 -0
  571. package/webmap/convert-label.d.ts +13 -0
  572. package/webmap/convert-label.d.ts.map +1 -0
  573. package/webmap/convert-label.js +125 -0
  574. package/webmap/convert-layer.d.ts +22 -0
  575. package/webmap/convert-layer.d.ts.map +1 -0
  576. package/webmap/convert-layer.js +181 -0
  577. package/webmap/convert-popup.d.ts +34 -0
  578. package/webmap/convert-popup.d.ts.map +1 -0
  579. package/webmap/convert-popup.js +68 -0
  580. package/webmap/convert-renderer.d.ts +17 -0
  581. package/webmap/convert-renderer.d.ts.map +1 -0
  582. package/webmap/convert-renderer.js +161 -0
  583. package/webmap/convert-symbol.d.ts +17 -0
  584. package/webmap/convert-symbol.d.ts.map +1 -0
  585. package/webmap/convert-symbol.js +122 -0
  586. package/webmap/index.d.ts +44 -0
  587. package/webmap/index.d.ts.map +1 -0
  588. package/webmap/index.js +33 -0
  589. package/webmap/parse.d.ts +25 -0
  590. package/webmap/parse.d.ts.map +1 -0
  591. package/webmap/parse.js +83 -0
  592. package/webmap/types.d.ts +264 -0
  593. package/webmap/types.d.ts.map +1 -0
  594. package/webmap/types.js +13 -0
  595. package/webmap/warnings.d.ts +30 -0
  596. package/webmap/warnings.d.ts.map +1 -0
  597. package/webmap/warnings.js +37 -0
package/core/client.js ADDED
@@ -0,0 +1,1664 @@
1
+ import { HONUA_DEFAULT_METADATA_STALE_IF_ERROR_MS, createHonuaCacheState, honuaCacheValidatorFromHeaders, honuaMetadataRequestHeaders, isHonuaCacheEntryFresh, normalizeHonuaMetadataRequestOptions, withHonuaCacheState, withoutHonuaCacheState, } from "./cache-state.js";
2
+ import { HONUA_MINIMUM_SUPPORTED_SERVER_VERSION, MINIMUM_SUPPORTED_SERVER_RELEASE_CHANNEL, describeCompatibilityError, evaluateCompatibility, parseCompatibilityEnvelope, } from "./compatibility.js";
3
+ import { HonuaHttpError, HonuaNetworkError, HonuaTimeoutError } from "./errors.js";
4
+ import { applyEdits, exportMap, findMap, getFeatureServiceMetadata, getLayerMetadata, getMapLayerMetadata, getMapLegend, getMapServiceMetadata, identifyMap, listServices, queryFeaturesRest, queryMapLayer, queryMapRelatedRecords, queryRelatedRecords, } from "./geoservices.js";
5
+ import { HonuaOdataEntitySet } from "./odata.js";
6
+ import { createOgcItem, deleteOgcItem, getOgcCollection, getOgcFeaturesConformance, getOgcFeaturesLanding, getOgcItem, getOgcQueryables, listOgcCollections, listOgcItems, patchOgcItem, replaceOgcItem, } from "./ogc-features.js";
7
+ import { HonuaOgcMaps, getOgcMapImage, getOgcMapsConformance, getOgcMapsLanding } from "./ogc-maps.js";
8
+ import { HonuaOgcProcesses, cancelOgcProcessJob, executeOgcProcess, getOgcProcess, getOgcProcessJob, getOgcProcessJobResults, getOgcProcessesConformance, getOgcProcessesLanding, listOgcProcesses, } from "./ogc-processes.js";
9
+ import { HonuaOgcRecords, fetchOgcRecordRaw, fetchOgcRecordsRaw, getOgcRecord, getOgcRecordCollection, getOgcRecordsConformance, getOgcRecordsLanding, listOgcRecordCollections, searchOgcRecords, } from "./ogc-records.js";
10
+ import { HonuaOgcTiles, fetchOgcTile, getOgcCollectionTileset, getOgcTileMatrixSet, getOgcTilesConformance, getOgcTilesLanding, listOgcCollectionTilesets, listOgcTileMatrixSets, } from "./ogc-tiles.js";
11
+ import { stripQuery, trimTrailingSlashes } from "./path-utils.js";
12
+ import { decodePbfQueryResponse, isPbfResponse } from "./pbf-decoder.js";
13
+ import { createGeoServicesGpAdapter, createGeospatialGrpcProcessAdapter, createHonuaProcessRunner, createOgcProcessesAdapter, } from "./process-runner.js";
14
+ import { DEFAULT_RETRY_METHODS, createTimeoutSignal, normalizeNetworkError, normalizeRetryOptions, normalizeTimeoutMs, resolveGrpcRetryDelayMs, resolveRetryDelayMs, shouldRetryGrpcCall, shouldRetryRequest, sleep, toHttpError, } from "./request-pipeline.js";
15
+ import { HonuaStacSearch, getStacCollection, getStacItem, getStacLanding, listStacCollections, searchStac, } from "./stac.js";
16
+ import { HonuaFeatureLayer, HonuaGeometryService, HonuaGeoprocessingService, HonuaImageService, HonuaMapLayer, HonuaMapService, HonuaOgcFeatures, HonuaService, } from "./surfaces.js";
17
+ import { HonuaWfs } from "./wfs.js";
18
+ import { mergeHeaders } from "./wire-shared.js";
19
+ import { HonuaWms, getWmsCapabilities, getWmsFeatureInfo, getWmsLegend, getWmsMap } from "./wms.js";
20
+ import { HonuaWmts, fetchWmtsTile, getWmtsCapabilities, getWmtsFeatureInfo } from "./wmts.js";
21
+ // Re-exported so the long-standing `@honua/sdk-js` public export path
22
+ // (`export { HONUA_MINIMUM_SUPPORTED_SERVER_VERSION } from "./core/client.js"`)
23
+ // is preserved now that the constant lives in the compatibility module.
24
+ export { HONUA_MINIMUM_SUPPORTED_SERVER_VERSION } from "./compatibility.js";
25
+ function normalizeBaseUrl(baseUrl) {
26
+ return trimTrailingSlashes(baseUrl);
27
+ }
28
+ function normalizePath(path) {
29
+ if (path.startsWith("http://") || path.startsWith("https://")) {
30
+ return path;
31
+ }
32
+ return path.startsWith("/") ? path : `/${path}`;
33
+ }
34
+ function isAbsoluteHttpUrl(value) {
35
+ return value.startsWith("http://") || value.startsWith("https://");
36
+ }
37
+ function resolveRequestUrl(baseUrl, path) {
38
+ if (isAbsoluteHttpUrl(path)) {
39
+ if (!isAbsoluteHttpUrl(baseUrl)) {
40
+ throw new Error("Absolute request URLs are not allowed when baseUrl is relative.");
41
+ }
42
+ const baseOrigin = new URL(baseUrl).origin;
43
+ const requestUrl = new URL(path);
44
+ if (requestUrl.origin !== baseOrigin) {
45
+ throw new Error(`Cross-origin request URL is not allowed: ${path}`);
46
+ }
47
+ return requestUrl.toString();
48
+ }
49
+ return `${baseUrl}${path}`;
50
+ }
51
+ /**
52
+ * The maximum number of HTTP redirects {@link HonuaClient.fetchWithSafeRedirects}
53
+ * will follow before giving up. Mirrors the conventional browser/undici limit of 20.
54
+ */
55
+ const MAX_SAFE_REDIRECTS = 20;
56
+ /**
57
+ * HTTP status codes that represent a redirect carrying a `Location` header.
58
+ */
59
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
60
+ /**
61
+ * Resolve a redirect `Location` (which may be relative) against the URL that
62
+ * produced the redirect and assert it stays on the configured base origin.
63
+ *
64
+ * The SDK attaches the API key as a custom `X-API-Key` header. The Fetch/undici
65
+ * runtime only strips `Authorization`/`Cookie`/`Host` on a cross-origin redirect,
66
+ * so custom auth headers like `X-API-Key` would otherwise be replayed to an
67
+ * attacker-controlled `Location` host. To prevent that credential disclosure we
68
+ * never auto-follow redirects; we re-run the same origin guard used at request
69
+ * construction and refuse to follow any redirect that leaves the base origin.
70
+ *
71
+ * @throws if the target origin differs from the base origin, or if the
72
+ * `Location` header is missing/unparsable.
73
+ */
74
+ function resolveRedirectUrl(baseUrl, fromUrl, location) {
75
+ if (!location) {
76
+ throw new Error("Redirect response is missing a Location header.");
77
+ }
78
+ let target;
79
+ try {
80
+ target = new URL(location, fromUrl);
81
+ }
82
+ catch {
83
+ throw new Error(`Redirect response has an invalid Location header: ${location}`);
84
+ }
85
+ // Re-run the base-origin guard against the redirect target so the API key is
86
+ // never forwarded off the configured origin.
87
+ return resolveRequestUrl(baseUrl, target.toString());
88
+ }
89
+ function normalizeInterceptorRequestUrl(baseUrl, url) {
90
+ if (isAbsoluteHttpUrl(url)) {
91
+ return resolveRequestUrl(baseUrl, url);
92
+ }
93
+ if (!isAbsoluteHttpUrl(baseUrl)) {
94
+ return url;
95
+ }
96
+ return resolveRequestUrl(baseUrl, normalizePath(url));
97
+ }
98
+ const DEFAULT_AUTH_REFRESH_SKEW_MS = 60_000;
99
+ const DEFAULT_METADATA_CACHE_MAX_ENTRIES = 256;
100
+ /**
101
+ * The main Honua HTTP/gRPC-Web client.
102
+ *
103
+ * `HonuaClient` is the protocol-aware entry point into the Honua server. It speaks
104
+ * GeoServices (FeatureServer, MapServer, ImageServer, GeometryServer, GPServer),
105
+ * OGC API Features / Tiles / Maps / Processes, STAC, WMS, WMTS, WFS 2.0, and OData v4,
106
+ * with one consistent request/response shape, capability negotiation, optional retries,
107
+ * pluggable auth, and a small in-process metadata cache.
108
+ *
109
+ * For cross-protocol code that does not need to know the underlying service shape,
110
+ * prefer the protocol-neutral {@link createDataset} contract from `@honua/sdk-js/contract`
111
+ * — it wraps this client and exposes a single `Source.query(...)` surface that throws
112
+ * {@link HonuaCapabilityNotSupportedError} when a protocol cannot satisfy the request.
113
+ *
114
+ * @example Basic usage
115
+ * ```ts
116
+ * import { HonuaClient } from "@honua/sdk-js/honua";
117
+ *
118
+ * const client = new HonuaClient({
119
+ * baseUrl: "https://your-honua-server.example",
120
+ * apiKey: process.env.HONUA_API_KEY,
121
+ * });
122
+ *
123
+ * const { supported, reasons } = await client.checkCompatibility();
124
+ * if (!supported) {
125
+ * throw new Error(`Unsupported Honua server: ${reasons.join("; ")}`);
126
+ * }
127
+ *
128
+ * const result = await client.queryFeatures({
129
+ * serviceId: "natural-earth",
130
+ * layerId: 0,
131
+ * where: "1=1",
132
+ * outFields: ["*"],
133
+ * returnGeometry: true,
134
+ * resultRecordCount: 25,
135
+ * });
136
+ *
137
+ * console.log(`Loaded ${result.features?.length ?? 0} features`);
138
+ * ```
139
+ *
140
+ * @example Per-service fluent wrappers
141
+ * ```ts
142
+ * const parcels = client.featureLayer<{ NAME: string }>("parcels", 0);
143
+ * const items = await client.ogcFeatures();
144
+ * const wms = client.wms("usgs-imagery");
145
+ * ```
146
+ *
147
+ * @public
148
+ */
149
+ export class HonuaClient {
150
+ /** The minimum Honua server version this SDK is contractually tested against. */
151
+ static minimumSupportedServerVersion = HONUA_MINIMUM_SUPPORTED_SERVER_VERSION;
152
+ static minimumSupportedServerReleaseChannel = MINIMUM_SUPPORTED_SERVER_RELEASE_CHANNEL;
153
+ baseUrl;
154
+ fetchFn;
155
+ defaultHeaders;
156
+ authProvider;
157
+ authRefreshSkewMs;
158
+ interceptors;
159
+ timeoutMs;
160
+ retryOptions;
161
+ preferBinary;
162
+ transport;
163
+ /**
164
+ * Low-level transport handed to the per-protocol wire modules. Binds the
165
+ * client's private request primitives so each protocol family builds its own
166
+ * URLs/params/parsing without reaching into the client.
167
+ */
168
+ protocolTransport;
169
+ serverCompatibilityCache;
170
+ authCredentialsCache;
171
+ authRefreshPromise;
172
+ connectClient;
173
+ metadataCache = new Map();
174
+ /**
175
+ * Create a new `HonuaClient`.
176
+ *
177
+ * @param options - Connection, auth, transport, retry, and interceptor configuration.
178
+ * See {@link HonuaClientOptions} for every field and `@example` blocks for common shapes.
179
+ *
180
+ * @example Minimal
181
+ * ```ts
182
+ * const client = new HonuaClient({ baseUrl: "https://your-honua-server.example" });
183
+ * ```
184
+ *
185
+ * @example With API key + retries + timeout
186
+ * ```ts
187
+ * const client = new HonuaClient({
188
+ * baseUrl: "https://your-honua-server.example",
189
+ * apiKey: process.env.HONUA_API_KEY,
190
+ * retry: { maxRetries: 3 },
191
+ * timeoutMs: 30_000,
192
+ * });
193
+ * ```
194
+ */
195
+ constructor(options) {
196
+ this.baseUrl = normalizeBaseUrl(options.baseUrl);
197
+ // Bind to `globalThis` at assignment: the default browser `fetch` is an
198
+ // unbound `window.fetch`, and invoking it as `this.fetchFn(...)` rebinds
199
+ // its receiver to the client instance, which browsers reject with
200
+ // "TypeError: Illegal invocation". Binding is a no-op for Node's undici
201
+ // fetch and for caller-supplied wrappers (arrow/bound functions ignore it).
202
+ this.fetchFn = (options.fetchFn ?? fetch).bind(globalThis);
203
+ const headers = {};
204
+ if (options.apiKey) {
205
+ headers["X-API-Key"] = options.apiKey;
206
+ }
207
+ if (options.bearerToken) {
208
+ headers.Authorization = `Bearer ${options.bearerToken}`;
209
+ }
210
+ this.defaultHeaders = headers;
211
+ this.authProvider = normalizeAuthProvider(options.auth);
212
+ this.authRefreshSkewMs = normalizeAuthRefreshSkewMs(options.authRefreshSkewMs);
213
+ this.interceptors = options.interceptors ?? [];
214
+ this.timeoutMs = normalizeTimeoutMs(options.timeoutMs);
215
+ this.retryOptions = normalizeRetryOptions(options.retry);
216
+ this.preferBinary = options.preferBinary === true;
217
+ this.transport = options.transport ?? "rest";
218
+ this.protocolTransport = {
219
+ baseUrl: this.baseUrl,
220
+ requestJson: (method, path, init, signal) => this.requestJson(method, path, init, signal),
221
+ requestText: (method, path, requestTextOptions) => this.requestText(method, path, requestTextOptions),
222
+ requestBytes: (method, path, accept, init, signal) => this.requestBytes(method, path, accept, init, signal),
223
+ requestCachedMetadataJson: (cacheKey, path, metadataOptions) => this.requestCachedMetadataJson(cacheKey, path, metadataOptions),
224
+ pipelineFetch: (method, path, init, callerSignal, pipelineOptions) => this.pipelineFetch(method, path, init, callerSignal, pipelineOptions),
225
+ requestBinaryWithJsonFallback: (method, path, params, signal) => this.requestBinaryWithJsonFallback(method, path, params, signal),
226
+ };
227
+ if (this.transport === "grpc-web") {
228
+ this.initConnectClient();
229
+ }
230
+ }
231
+ /**
232
+ * Connect interceptor that injects the same credentials the REST pipeline
233
+ * uses (`apiKey`/`bearerToken` from {@link defaultHeaders} plus any provider
234
+ * credentials resolved via {@link resolveAuthHeaders}) onto every gRPC-web
235
+ * call. Without it the advertised `transport: "grpc-web"` path would send
236
+ * every RPC unauthenticated.
237
+ */
238
+ buildConnectAuthInterceptor() {
239
+ return (next) => async (req) => {
240
+ const headers = await this.composeHeaders();
241
+ for (const [key, value] of Object.entries(headers)) {
242
+ req.header.set(key, value);
243
+ }
244
+ return next(req);
245
+ };
246
+ }
247
+ /**
248
+ * Connect interceptor that applies the SDK's configured `retry` policy to
249
+ * gRPC-web calls, mirroring the REST request pipeline. It retries only
250
+ * replay-safe **unary** calls (server-streaming responses cannot be safely
251
+ * replayed mid-iteration, the gRPC analog of the REST
252
+ * {@link DEFAULT_RETRY_METHODS} idempotency gate) on the transient
253
+ * {@link shouldRetryGrpcCall} status codes, backing off with the shared
254
+ * exponential-with-jitter math (honoring any `retry-after` metadata) and
255
+ * stopping immediately once the call's abort signal fires (caller abort or the
256
+ * `timeoutMs` deadline). It wraps the auth interceptor so each attempt
257
+ * re-resolves fresh credentials.
258
+ */
259
+ buildConnectRetryInterceptor() {
260
+ return (next) => async (req) => {
261
+ // Only retry replay-safe unary calls; mirror REST idempotency scoping.
262
+ if (req.stream || !this.retryOptions) {
263
+ return next(req);
264
+ }
265
+ const retryOptions = this.retryOptions;
266
+ for (let attempt = 0;; attempt += 1) {
267
+ try {
268
+ return await next(req);
269
+ }
270
+ catch (error) {
271
+ const aborted = req.signal?.aborted === true;
272
+ if (!shouldRetryGrpcCall(retryOptions, attempt, error, aborted)) {
273
+ throw error;
274
+ }
275
+ await sleep(resolveGrpcRetryDelayMs(retryOptions, attempt, error), req.signal);
276
+ }
277
+ }
278
+ };
279
+ }
280
+ /**
281
+ * Shared gRPC-web transport options. The auth interceptor mirrors the REST
282
+ * credential pipeline, the retry interceptor (when `retry` is configured)
283
+ * applies the same retry/backoff policy as REST, and `defaultTimeoutMs`
284
+ * honors the documented `timeoutMs` contract on every RPC (per-call abort
285
+ * signals are threaded by the individual RPC methods).
286
+ */
287
+ connectTransportOptions() {
288
+ // Order matters: the retry interceptor must wrap the auth interceptor so
289
+ // every replayed attempt re-runs auth and re-resolves credential headers.
290
+ const interceptors = [this.buildConnectAuthInterceptor()];
291
+ if (this.retryOptions) {
292
+ interceptors.unshift(this.buildConnectRetryInterceptor());
293
+ }
294
+ return {
295
+ baseUrl: this.baseUrl,
296
+ fetch: this.fetchFn,
297
+ interceptors,
298
+ ...(this.timeoutMs !== undefined ? { defaultTimeoutMs: this.timeoutMs } : {}),
299
+ };
300
+ }
301
+ initConnectClient() {
302
+ // Dynamic imports are used to avoid bundling Connect dependencies
303
+ // when only the REST transport is used. The imports are resolved
304
+ // at module level since they are static ESM imports.
305
+ import("@connectrpc/connect").then(({ createClient }) => import("@connectrpc/connect-web").then(({ createGrpcWebTransport }) => import("../gen/honua/v1/feature_service_pb.js").then(({ FeatureService }) => {
306
+ const transport = createGrpcWebTransport(this.connectTransportOptions());
307
+ this.connectClient = createClient(FeatureService, transport);
308
+ })));
309
+ }
310
+ async ensureConnectClient() {
311
+ if (this.connectClient) {
312
+ return this.connectClient;
313
+ }
314
+ // If initConnectClient hasn't resolved yet, wait for it
315
+ const { createClient } = await import("@connectrpc/connect");
316
+ const { createGrpcWebTransport } = await import("@connectrpc/connect-web");
317
+ const { FeatureService } = await import("../gen/honua/v1/feature_service_pb.js");
318
+ const transport = createGrpcWebTransport(this.connectTransportOptions());
319
+ this.connectClient = createClient(FeatureService, transport);
320
+ return this.connectClient;
321
+ }
322
+ /**
323
+ * Perform a fetch that never auto-follows a cross-origin redirect.
324
+ *
325
+ * Browsers/undici forward custom request headers (such as the SDK's
326
+ * `X-API-Key`) across redirects, stripping only `Authorization`/`Cookie`/`Host`
327
+ * cross-origin. To avoid leaking the API key to an attacker-supplied
328
+ * `Location` host, this issues every request with `redirect: "manual"` and
329
+ * only follows a redirect when its target origin still matches the configured
330
+ * base origin (re-running the {@link resolveRequestUrl} origin guard).
331
+ * Cross-origin redirects throw before the credentialed request is replayed.
332
+ */
333
+ async fetchWithSafeRedirects(url, init) {
334
+ let currentUrl = url;
335
+ let currentInit = init;
336
+ for (let redirects = 0;; redirects += 1) {
337
+ const response = await this.fetchFn(currentUrl, { ...currentInit, redirect: "manual" });
338
+ // `redirect: "manual"` surfaces redirects as status 3xx with a usable
339
+ // `Location` header (and, in fetch, an "opaqueredirect" type with status 0
340
+ // in some runtimes — treated as a same-origin-unverifiable redirect we
341
+ // must refuse to follow blindly).
342
+ if (response.type === "opaqueredirect") {
343
+ throw new HonuaNetworkError("Refusing to follow an opaque cross-origin redirect; the request's API key would be leaked to the redirect target.", undefined);
344
+ }
345
+ if (!REDIRECT_STATUSES.has(response.status)) {
346
+ return response;
347
+ }
348
+ if (redirects >= MAX_SAFE_REDIRECTS) {
349
+ throw new HonuaNetworkError(`Exceeded the maximum of ${MAX_SAFE_REDIRECTS} redirects.`, undefined);
350
+ }
351
+ // Re-validate the redirect target against the base origin. A cross-origin
352
+ // target throws here, so the API key is never replayed off-origin.
353
+ const location = response.headers.get("location");
354
+ const nextUrl = resolveRedirectUrl(this.baseUrl, currentUrl, location);
355
+ // Apply the standard Fetch redirect method/body rewriting so the followed
356
+ // (same-origin) request matches what the runtime would have done natively:
357
+ // 303 always becomes GET; 301/302 turn a non-GET/HEAD into GET. The body
358
+ // is dropped whenever the method changes to GET.
359
+ const method = (currentInit.method ?? "GET").toUpperCase();
360
+ const downgradeToGet = response.status === 303 ||
361
+ ((response.status === 301 || response.status === 302) && method !== "GET" && method !== "HEAD");
362
+ currentInit = downgradeToGet ? { ...currentInit, method: "GET", body: null } : currentInit;
363
+ currentUrl = nextUrl;
364
+ // Drain the redirect body so the underlying connection can be reused.
365
+ await response.body?.cancel().catch(() => undefined);
366
+ }
367
+ }
368
+ static async loadGrpcAdapter() {
369
+ return import("./grpc-adapter.js");
370
+ }
371
+ get isGrpcWeb() {
372
+ return this.transport === "grpc-web";
373
+ }
374
+ /**
375
+ * Normalized base URL the client was constructed with (trailing slashes
376
+ * trimmed). Helpers that build absolute URLs without going through
377
+ * `request()` (e.g. tile URL generators) read this so they produce the
378
+ * same origin and base path the server actually serves from.
379
+ */
380
+ get serverBaseUrl() {
381
+ return this.baseUrl;
382
+ }
383
+ /**
384
+ * Force-refresh credentials from the configured auth provider. The SDK
385
+ * keeps the result in memory only; callers own durable and secure storage.
386
+ */
387
+ async refreshAuthCredentials(reason = "manual") {
388
+ const cached = await this.resolveAuthCredentials({
389
+ forceRefresh: true,
390
+ reason,
391
+ });
392
+ return cached?.credentials;
393
+ }
394
+ /** Drop cached provider credentials without calling a revocation endpoint. */
395
+ clearAuthCredentials() {
396
+ this.authCredentialsCache = undefined;
397
+ }
398
+ /**
399
+ * Revoke the currently cached credentials through the provider, when it
400
+ * exposes a revocation hook, then clear the SDK's in-memory cache.
401
+ */
402
+ async revokeAuthCredentials(context = { reason: "manual" }) {
403
+ const cached = this.authCredentialsCache;
404
+ this.authCredentialsCache = undefined;
405
+ if (cached && this.authProvider?.revokeCredentials) {
406
+ await this.authProvider.revokeCredentials(cached.credentials, context);
407
+ }
408
+ }
409
+ /**
410
+ * Drive a paginated FeatureServer query as an async generator. Each yielded
411
+ * chunk is a `HonuaFeature[]` slice; iteration stops when the server stops
412
+ * advertising `exceededTransferLimit`. Lower-level than
413
+ * `dataset.source(...).queryAll()` but suitable for streaming pipelines that
414
+ * want backpressure between pages.
415
+ *
416
+ * @example
417
+ * ```ts
418
+ * for await (const page of client.queryFeaturesStream({ serviceId: "parcels", layerId: 0, where: "1=1" })) {
419
+ * process(page);
420
+ * }
421
+ * ```
422
+ */
423
+ async *queryFeaturesStream(request) {
424
+ const client = await this.ensureConnectClient();
425
+ const grpcAdapter = await HonuaClient.loadGrpcAdapter();
426
+ const protoRequest = grpcAdapter.toProtoQueryRequest(request);
427
+ yield* grpcAdapter.streamProtoPages(client.queryFeaturesStream(protoRequest, request.signal ? { signal: request.signal } : undefined));
428
+ }
429
+ service(serviceId) {
430
+ return new HonuaService({
431
+ client: this,
432
+ serviceId,
433
+ });
434
+ }
435
+ /**
436
+ * Construct a typed wrapper for a single FeatureServer layer.
437
+ *
438
+ * The returned {@link HonuaFeatureLayer} carries the same `serviceId` / `layerId`
439
+ * on every call so you can write `await layer.queryFeatures({ where: "..." })`
440
+ * without restating the address.
441
+ *
442
+ * @typeParam T - The attribute shape of features in this layer.
443
+ *
444
+ * @example
445
+ * ```ts
446
+ * const parcels = client.featureLayer<{ NAME: string; STATUS: string }>("parcels", 0);
447
+ * const { features } = await parcels.queryFeatures({ where: "STATUS = 'ACTIVE'" });
448
+ * ```
449
+ */
450
+ featureLayer(serviceId, layerId) {
451
+ return new HonuaFeatureLayer({
452
+ client: this,
453
+ serviceId,
454
+ layerId,
455
+ });
456
+ }
457
+ mapService(serviceId) {
458
+ return new HonuaMapService({
459
+ client: this,
460
+ serviceId,
461
+ });
462
+ }
463
+ mapLayer(serviceId, layerId) {
464
+ return new HonuaMapLayer({
465
+ client: this,
466
+ serviceId,
467
+ layerId,
468
+ });
469
+ }
470
+ /**
471
+ * Construct the OGC API Features client wrapper.
472
+ *
473
+ * Use this to walk collections (`landing()`, `conformance()`, `collections()`),
474
+ * read items (`items()`, `item()`), and apply edits (`create*` / `replace*` /
475
+ * `patch*` / `delete*`) against an OGC API Features endpoint exposed by the
476
+ * Honua server.
477
+ *
478
+ * @example
479
+ * ```ts
480
+ * const features = client.ogcFeatures();
481
+ * const collections = await features.collections();
482
+ * const items = await features.items("parcels", { limit: 100 });
483
+ * ```
484
+ */
485
+ ogcFeatures() {
486
+ return new HonuaOgcFeatures({
487
+ client: this,
488
+ });
489
+ }
490
+ ogcTiles() {
491
+ return new HonuaOgcTiles({ client: this });
492
+ }
493
+ ogcMaps() {
494
+ return new HonuaOgcMaps({ client: this });
495
+ }
496
+ ogcRecords() {
497
+ return new HonuaOgcRecords({ client: this });
498
+ }
499
+ wms(serviceId) {
500
+ return new HonuaWms({ client: this, serviceId });
501
+ }
502
+ wmts(serviceId) {
503
+ return new HonuaWmts({ client: this, serviceId });
504
+ }
505
+ ogcProcesses() {
506
+ return new HonuaOgcProcesses({ client: this });
507
+ }
508
+ stac() {
509
+ return new HonuaStacSearch({ client: this });
510
+ }
511
+ imageService(serviceId) {
512
+ return new HonuaImageService({ client: this, serviceId });
513
+ }
514
+ geometryService() {
515
+ return new HonuaGeometryService({ client: this });
516
+ }
517
+ geoprocessing(serviceId, taskName) {
518
+ return new HonuaGeoprocessingService({ client: this, serviceId, taskName });
519
+ }
520
+ processRunner(adapter) {
521
+ return createHonuaProcessRunner(adapter);
522
+ }
523
+ ogcProcessRunner() {
524
+ return createHonuaProcessRunner(createOgcProcessesAdapter(this.ogcProcesses()));
525
+ }
526
+ geoprocessingRunner(serviceId, taskName) {
527
+ return createHonuaProcessRunner(createGeoServicesGpAdapter(this.geoprocessing(serviceId, taskName)));
528
+ }
529
+ geospatialGrpcProcessRunner(processClient) {
530
+ return createHonuaProcessRunner(createGeospatialGrpcProcessAdapter(processClient));
531
+ }
532
+ wfs(endpointUrl = "/wfs", options = {}) {
533
+ return new HonuaWfs({ client: this, endpointUrl, version: options.version });
534
+ }
535
+ odata(entitySet, options = {}) {
536
+ return new HonuaOdataEntitySet({ client: this, entitySet, basePath: options.basePath });
537
+ }
538
+ clearMetadataCache(options = {}) {
539
+ if (!options.keyPrefix) {
540
+ this.metadataCache.clear();
541
+ return;
542
+ }
543
+ const prefix = `metadata:${options.keyPrefix}`;
544
+ for (const key of this.metadataCache.keys()) {
545
+ if (key.startsWith(prefix)) {
546
+ this.metadataCache.delete(key);
547
+ }
548
+ }
549
+ }
550
+ async listServices(formatOrOptions = "json", options = {}) {
551
+ const format = typeof formatOrOptions === "string" ? formatOrOptions : "json";
552
+ const metadataOptions = typeof formatOrOptions === "string" ? options : formatOrOptions;
553
+ return listServices(this.protocolTransport, format, metadataOptions);
554
+ }
555
+ /**
556
+ * Fetch and parse the server's compatibility contract from `GET /api/v1/admin/capabilities`.
557
+ *
558
+ * The first call populates an in-process cache; subsequent calls return the cached value
559
+ * unless `options.refresh` is `true`. Use {@link HonuaClient.checkCompatibility} instead
560
+ * when you want a non-throwing pass/fail signal with a list of reasons.
561
+ *
562
+ * @throws {@link HonuaError} when the server response cannot be parsed into a valid
563
+ * compatibility envelope (missing `serverVersion`, `controlPlaneApi`, etc.).
564
+ *
565
+ * @example
566
+ * ```ts
567
+ * const contract = await client.getCompatibility();
568
+ * console.log(contract.serverVersion, contract.releaseChannel);
569
+ * console.log(contract.metadataSchemas);
570
+ * ```
571
+ */
572
+ async getCompatibility(options = {}) {
573
+ if (!options.refresh && this.serverCompatibilityCache) {
574
+ return this.serverCompatibilityCache;
575
+ }
576
+ const response = (await this.requestJson("GET", "/api/v1/admin/capabilities", undefined, options.signal));
577
+ const compatibility = parseCompatibilityEnvelope(response);
578
+ this.serverCompatibilityCache = compatibility;
579
+ return compatibility;
580
+ }
581
+ /**
582
+ * Probe the server's compatibility contract and return a structured pass/fail status.
583
+ *
584
+ * Unlike {@link HonuaClient.getCompatibility}, this method does not throw on transport
585
+ * or parse failures — those are reported as `supported: false` with a human-readable
586
+ * `reasons` entry. Use this at app startup to fail loudly before exercising admin or
587
+ * control-plane flows.
588
+ *
589
+ * @example
590
+ * ```ts
591
+ * const { supported, reasons } = await client.checkCompatibility();
592
+ * if (!supported) {
593
+ * throw new Error(`Unsupported Honua server: ${reasons.join("; ")}`);
594
+ * }
595
+ * ```
596
+ */
597
+ async checkCompatibility(options = {}) {
598
+ try {
599
+ const compatibility = await this.getCompatibility(options);
600
+ const reasons = evaluateCompatibility(compatibility);
601
+ return {
602
+ supported: reasons.length === 0,
603
+ minimumSupportedServerVersion: HonuaClient.minimumSupportedServerVersion,
604
+ compatibility,
605
+ reasons,
606
+ };
607
+ }
608
+ catch (error) {
609
+ return {
610
+ supported: false,
611
+ minimumSupportedServerVersion: HonuaClient.minimumSupportedServerVersion,
612
+ reasons: [describeCompatibilityError(error)],
613
+ };
614
+ }
615
+ }
616
+ /**
617
+ * Returns `true` if the server's `data.compatibility.features` map advertises the
618
+ * given coarse capability. Use this to gate experimental or admin-only workflows.
619
+ *
620
+ * @example
621
+ * ```ts
622
+ * if (await client.supportsFeature("manifestApply")) {
623
+ * // safe to call the manifest apply admin endpoint
624
+ * }
625
+ * ```
626
+ */
627
+ async supportsFeature(feature, options = {}) {
628
+ const compatibility = await this.getCompatibility(options);
629
+ return compatibility.features[feature];
630
+ }
631
+ /**
632
+ * Scan a supported migration source through the admin import scanner.
633
+ *
634
+ * A successful HTTP response means the server returned a deterministic
635
+ * inventory artifact; callers still need to inspect
636
+ * `scanCompleteness.status`, which can be `"failed"` on `200 OK`.
637
+ */
638
+ async scanMigrationSource(request) {
639
+ const { signal, exportJson, ...body } = request;
640
+ const path = `/api/v1/admin/import/scan${exportJson ? "?export=json" : ""}`;
641
+ return this.requestJson("POST", path, {
642
+ headers: { "Content-Type": "application/json" },
643
+ body: JSON.stringify(body),
644
+ }, signal);
645
+ }
646
+ async request(request) {
647
+ const method = request.method ?? "GET";
648
+ const params = new URLSearchParams();
649
+ params.set("f", request.responseFormat ?? "json");
650
+ if (request.query) {
651
+ for (const [key, value] of Object.entries(request.query)) {
652
+ params.set(key, String(value));
653
+ }
654
+ }
655
+ const normalizedPath = normalizePath(request.path);
656
+ const pathWithQuery = mergePathWithQueryParams(normalizedPath, params);
657
+ return this.requestJson(method, pathWithQuery, {
658
+ headers: request.headers,
659
+ body: request.body,
660
+ }, request.signal);
661
+ }
662
+ /**
663
+ * Pipeline-aware JSON request that bypasses the GeoServices `f=json`
664
+ * convention used by {@link request}. Adapters whose protocols do not
665
+ * model `f=` (OData, OGC API, …) call this directly so they keep the
666
+ * shared auth / retry / timeout / interceptor pipeline without sending
667
+ * a query parameter the server would reject as `InvalidQueryOption`.
668
+ *
669
+ * Caller-supplied query parameters belong on `path` itself; `init`
670
+ * carries the body, headers, and abort signal. The default `Accept`
671
+ * header is `application/json`; pass an explicit `Accept` in
672
+ * `init.headers` to override.
673
+ */
674
+ async pipelineRequestJson(method, path, init, signal) {
675
+ return this.requestJson(method, path, init, signal);
676
+ }
677
+ async composeHeaders(...headersList) {
678
+ const authHeaders = await this.resolveAuthHeaders();
679
+ return mergeHeaders(this.defaultHeaders, authHeaders, ...headersList);
680
+ }
681
+ async resolveAuthHeaders() {
682
+ const cached = await this.resolveAuthCredentials({ forceRefresh: false });
683
+ if (!cached)
684
+ return undefined;
685
+ return authHeadersFromCredentials(cached.credentials);
686
+ }
687
+ async resolveAuthCredentials(options) {
688
+ if (!this.authProvider) {
689
+ return undefined;
690
+ }
691
+ const cached = this.authCredentialsCache;
692
+ if (!options.forceRefresh && cached && !isAuthCredentialsExpiring(cached, this.authRefreshSkewMs)) {
693
+ return cached;
694
+ }
695
+ if (this.authRefreshPromise) {
696
+ return this.authRefreshPromise;
697
+ }
698
+ const reason = options.reason ?? resolveAuthRefreshReason(cached);
699
+ this.authRefreshPromise = this.loadAuthCredentials(reason, options.forceRefresh, cached?.credentials);
700
+ try {
701
+ return await this.authRefreshPromise;
702
+ }
703
+ finally {
704
+ this.authRefreshPromise = undefined;
705
+ }
706
+ }
707
+ async loadAuthCredentials(reason, forceRefresh, previousCredentials) {
708
+ const credentials = normalizeAuthCredentials(await this.authProvider?.getCredentials({
709
+ reason,
710
+ forceRefresh,
711
+ ...(previousCredentials ? { previousCredentials } : {}),
712
+ }));
713
+ if (!credentials) {
714
+ this.authCredentialsCache = undefined;
715
+ return undefined;
716
+ }
717
+ const cached = {
718
+ credentials,
719
+ expiresAtMs: normalizeAuthExpiresAtMs(credentials.expiresAt),
720
+ };
721
+ this.authCredentialsCache = cached;
722
+ return cached;
723
+ }
724
+ /**
725
+ * Pipeline-aware request that returns the raw `Response` after the
726
+ * shared auth / retry / timeout / interceptor pipeline finishes
727
+ * successfully. Used by adapters that need to consume non-JSON bodies
728
+ * (OData `$metadata` XML, raw passthrough) without inheriting the
729
+ * `Accept: application/json` default of {@link pipelineRequestJson}.
730
+ *
731
+ * The returned `Response` is unconsumed — the caller picks `.json()`,
732
+ * `.text()`, or `.arrayBuffer()`. Non-2xx responses still throw the
733
+ * normalized `HonuaHttpError` (and trigger retries) so error handling
734
+ * matches every other client method.
735
+ */
736
+ async pipelineFetch(method, path, init, callerSignal, options = {}) {
737
+ const request = {
738
+ url: resolveRequestUrl(this.baseUrl, path),
739
+ path,
740
+ method,
741
+ init: {
742
+ method,
743
+ headers: await this.composeHeaders(init?.headers),
744
+ body: init?.body ?? null,
745
+ ...(init?.signal ? { signal: init.signal } : {}),
746
+ },
747
+ };
748
+ return this.executeRequest(request, {
749
+ callerSignal,
750
+ ...(options.okStatuses ? { okStatuses: options.okStatuses } : {}),
751
+ finalize: async (response, _durationMs, _request, runAfter) => {
752
+ await runAfter();
753
+ return response;
754
+ },
755
+ });
756
+ }
757
+ async requestCachedMetadataJson(cacheKey, path, options = {}) {
758
+ const metadataOptions = normalizeHonuaMetadataRequestOptions(options);
759
+ const keyFingerprint = `metadata:${cacheKey}`;
760
+ const cached = this.metadataCache.get(keyFingerprint);
761
+ const bypass = metadataOptions.cache === "bypass";
762
+ const now = Date.now();
763
+ const freshCachedEntry = cached ? isHonuaCacheEntryFresh(cached.cachedAtMs, now, metadataOptions.ttlMs) : false;
764
+ if (!bypass && !metadataOptions.refresh && cached && freshCachedEntry) {
765
+ return withHonuaCacheState(cached.body, createMetadataCacheState(cached, "hit", {
766
+ now,
767
+ ttlMs: metadataOptions.ttlMs,
768
+ staleIfErrorMs: metadataOptions.staleIfErrorMs,
769
+ }));
770
+ }
771
+ const request = {
772
+ url: resolveRequestUrl(this.baseUrl, path),
773
+ path,
774
+ method: "GET",
775
+ init: {
776
+ method: "GET",
777
+ headers: await this.composeHeaders(honuaMetadataRequestHeaders({
778
+ accept: "application/json",
779
+ refresh: metadataOptions.refresh || Boolean(cached),
780
+ bypass,
781
+ validator: cached?.validator,
782
+ })),
783
+ },
784
+ };
785
+ return this.executeRequest(request, {
786
+ callerSignal: metadataOptions.signal,
787
+ // stale-if-error fallback on a terminal network or HTTP failure.
788
+ onTerminalError: (error) => this.staleMetadataFallback(cached, metadataOptions, error),
789
+ // 304 Not Modified: revalidate the cached entry without re-parsing a body.
790
+ shortCircuit: async (response, _durationMs, _currentRequest, runAfter) => {
791
+ if (response.status !== 304 || !cached || bypass) {
792
+ return undefined;
793
+ }
794
+ await runAfter();
795
+ const updatedEntry = {
796
+ ...cached,
797
+ cachedAtMs: Date.now(),
798
+ ...((honuaCacheValidatorFromHeaders(response.headers) ?? cached.validator)
799
+ ? { validator: honuaCacheValidatorFromHeaders(response.headers) ?? cached.validator }
800
+ : {}),
801
+ };
802
+ this.setMetadataCacheEntry(keyFingerprint, updatedEntry);
803
+ return {
804
+ value: withHonuaCacheState(updatedEntry.body, createMetadataCacheState(updatedEntry, "refreshed", {
805
+ now: Date.now(),
806
+ ttlMs: metadataOptions.ttlMs,
807
+ staleIfErrorMs: metadataOptions.staleIfErrorMs,
808
+ revalidatedAt: new Date().toISOString(),
809
+ })),
810
+ };
811
+ },
812
+ finalize: async (response, durationMs, currentRequest, runAfter) => {
813
+ const body = await parseResponseBody(this.hasAfterInterceptors() ? response.clone() : response);
814
+ // GeoServices reports metadata failures (layer not found, invalid token
815
+ // => code 499, …) as HTTP 200 with a top-level `{error}` envelope.
816
+ // Detect it before caching so a transient auth/permission error is never
817
+ // persisted as "valid" metadata for the cache TTL and surfaced
818
+ // downstream as `undefined` field access instead of a HonuaHttpError.
819
+ const envelopeError = geoServicesEnvelopeError(response.status, body);
820
+ if (envelopeError) {
821
+ const stale = this.staleMetadataFallback(cached, metadataOptions, envelopeError);
822
+ if (stale)
823
+ return stale;
824
+ await this.applyErrorInterceptors({
825
+ request: cloneRequestContext(currentRequest),
826
+ error: envelopeError,
827
+ durationMs,
828
+ });
829
+ throw envelopeError;
830
+ }
831
+ await runAfter();
832
+ const cleanBody = withoutHonuaCacheState(body);
833
+ const validator = honuaCacheValidatorFromHeaders(response.headers);
834
+ const sourceUpdatedAt = response.headers.get("last-modified") ?? undefined;
835
+ const entry = {
836
+ body: cleanBody,
837
+ cachedAtMs: Date.now(),
838
+ keyFingerprint,
839
+ ...(validator ? { validator } : {}),
840
+ ...(sourceUpdatedAt ? { sourceUpdatedAt } : {}),
841
+ };
842
+ const status = bypass ? "bypass" : cached ? "refreshed" : "miss";
843
+ if (!bypass) {
844
+ this.setMetadataCacheEntry(keyFingerprint, entry);
845
+ }
846
+ return withHonuaCacheState(cleanBody, createMetadataCacheState(entry, status, {
847
+ now: Date.now(),
848
+ ttlMs: metadataOptions.ttlMs,
849
+ staleIfErrorMs: metadataOptions.staleIfErrorMs,
850
+ ...(status === "refreshed" ? { revalidatedAt: new Date().toISOString() } : {}),
851
+ }));
852
+ },
853
+ });
854
+ }
855
+ staleMetadataFallback(cached, options, error) {
856
+ if (!cached || options.cache === "bypass" || !options.staleIfError) {
857
+ return undefined;
858
+ }
859
+ const staleIfErrorMs = options.staleIfErrorMs ?? HONUA_DEFAULT_METADATA_STALE_IF_ERROR_MS;
860
+ if (Date.now() - cached.cachedAtMs > staleIfErrorMs) {
861
+ return undefined;
862
+ }
863
+ return withHonuaCacheState(cached.body, createMetadataCacheState(cached, "stale", {
864
+ now: Date.now(),
865
+ ttlMs: options.ttlMs,
866
+ staleIfErrorMs: options.staleIfErrorMs,
867
+ refreshErrorId: metadataRefreshErrorId(error),
868
+ }));
869
+ }
870
+ setMetadataCacheEntry(key, entry) {
871
+ this.metadataCache.set(key, entry);
872
+ while (this.metadataCache.size > DEFAULT_METADATA_CACHE_MAX_ENTRIES) {
873
+ const oldest = [...this.metadataCache.entries()].sort((a, b) => a[1].cachedAtMs - b[1].cachedAtMs)[0];
874
+ if (!oldest)
875
+ return;
876
+ this.metadataCache.delete(oldest[0]);
877
+ }
878
+ }
879
+ async getLayerMetadata(serviceId, layerId, options = {}) {
880
+ return getLayerMetadata(this.protocolTransport, serviceId, layerId, options);
881
+ }
882
+ async getFeatureServiceMetadata(serviceId, options = {}) {
883
+ return getFeatureServiceMetadata(this.protocolTransport, serviceId, options);
884
+ }
885
+ async getOgcFeaturesLanding(request = {}) {
886
+ return getOgcFeaturesLanding(this.protocolTransport, request);
887
+ }
888
+ async getOgcFeaturesConformance(request = {}) {
889
+ return getOgcFeaturesConformance(this.protocolTransport, request);
890
+ }
891
+ async listOgcCollections(request = {}) {
892
+ return listOgcCollections(this.protocolTransport, request);
893
+ }
894
+ async getOgcCollection(request) {
895
+ return getOgcCollection(this.protocolTransport, request);
896
+ }
897
+ async getOgcQueryables(request) {
898
+ return getOgcQueryables(this.protocolTransport, request);
899
+ }
900
+ async listOgcItems(request) {
901
+ return listOgcItems(this.protocolTransport, request);
902
+ }
903
+ async getOgcItem(request) {
904
+ return getOgcItem(this.protocolTransport, request);
905
+ }
906
+ async createOgcItem(request) {
907
+ return createOgcItem(this.protocolTransport, request);
908
+ }
909
+ async replaceOgcItem(request) {
910
+ return replaceOgcItem(this.protocolTransport, request);
911
+ }
912
+ async patchOgcItem(request) {
913
+ return patchOgcItem(this.protocolTransport, request);
914
+ }
915
+ async deleteOgcItem(request) {
916
+ await deleteOgcItem(this.protocolTransport, request);
917
+ }
918
+ // ── OGC API Tiles ───────────────────────────────────────────
919
+ async getOgcTilesLanding(request = {}) {
920
+ return getOgcTilesLanding(this.protocolTransport, request);
921
+ }
922
+ async getOgcTilesConformance(request = {}) {
923
+ return getOgcTilesConformance(this.protocolTransport, request);
924
+ }
925
+ async listOgcTileMatrixSets(request = {}) {
926
+ return listOgcTileMatrixSets(this.protocolTransport, request);
927
+ }
928
+ async getOgcTileMatrixSet(request) {
929
+ return getOgcTileMatrixSet(this.protocolTransport, request);
930
+ }
931
+ async listOgcCollectionTilesets(request) {
932
+ return listOgcCollectionTilesets(this.protocolTransport, request);
933
+ }
934
+ async getOgcCollectionTileset(request) {
935
+ return getOgcCollectionTileset(this.protocolTransport, request);
936
+ }
937
+ async fetchOgcTile(request) {
938
+ return fetchOgcTile(this.protocolTransport, request);
939
+ }
940
+ // ── OGC API Maps ────────────────────────────────────────────
941
+ async getOgcMapsLanding(request = {}) {
942
+ return getOgcMapsLanding(this.protocolTransport, request);
943
+ }
944
+ async getOgcMapsConformance(request = {}) {
945
+ return getOgcMapsConformance(this.protocolTransport, request);
946
+ }
947
+ async getOgcMapImage(request) {
948
+ return getOgcMapImage(this.protocolTransport, request);
949
+ }
950
+ // ── OGC API Records ────────────────────────────────────────
951
+ async getOgcRecordsLanding(request = {}) {
952
+ return getOgcRecordsLanding(this.protocolTransport, request);
953
+ }
954
+ async getOgcRecordsConformance(request = {}) {
955
+ return getOgcRecordsConformance(this.protocolTransport, request);
956
+ }
957
+ async listOgcRecordCollections(request = {}) {
958
+ return listOgcRecordCollections(this.protocolTransport, request);
959
+ }
960
+ async getOgcRecordCollection(request) {
961
+ return getOgcRecordCollection(this.protocolTransport, request);
962
+ }
963
+ async searchOgcRecords(request) {
964
+ return searchOgcRecords(this.protocolTransport, request);
965
+ }
966
+ async getOgcRecord(request) {
967
+ return getOgcRecord(this.protocolTransport, request);
968
+ }
969
+ async fetchOgcRecordsRaw(request) {
970
+ return fetchOgcRecordsRaw(this.protocolTransport, request);
971
+ }
972
+ async fetchOgcRecordRaw(request) {
973
+ return fetchOgcRecordRaw(this.protocolTransport, request);
974
+ }
975
+ // ── WMS 1.3 ─────────────────────────────────────────────────
976
+ /**
977
+ * Fetch and parse a WMS `GetCapabilities` document for the addressed
978
+ * service. The XML body decodes through `requestText`; the parsed
979
+ * shape is the typed `WmsCapabilities` envelope (no XML node leaks
980
+ * through the public surface).
981
+ */
982
+ async getWmsCapabilities(request) {
983
+ return getWmsCapabilities(this.protocolTransport, request);
984
+ }
985
+ /** Render a WMS `GetMap`. Returns the raw image bytes. */
986
+ async getWmsMap(request) {
987
+ return getWmsMap(this.protocolTransport, request);
988
+ }
989
+ /**
990
+ * Issue a WMS `GetFeatureInfo`. When `INFO_FORMAT=application/json`
991
+ * the JSON body decodes into the canonical `HonuaTypedFeature[]`
992
+ * shape; non-JSON formats round-trip on `bytes` so callers retain the
993
+ * raw payload behind the protocol escape hatch.
994
+ */
995
+ async getWmsFeatureInfo(request) {
996
+ return getWmsFeatureInfo(this.protocolTransport, request);
997
+ }
998
+ /**
999
+ * Fetch a WMS `GetLegendGraphic`. honua-server does not implement
1000
+ * GetLegendGraphic today; callers should branch on
1001
+ * `WmsCapabilities.request.getLegendGraphic` before invoking. When
1002
+ * the wire returns 5xx the underlying `HonuaHttpError` flows through.
1003
+ */
1004
+ async getWmsLegend(request) {
1005
+ return getWmsLegend(this.protocolTransport, request);
1006
+ }
1007
+ // ── WMTS 1.0 ────────────────────────────────────────────────
1008
+ async getWmtsCapabilities(request) {
1009
+ return getWmtsCapabilities(this.protocolTransport, request);
1010
+ }
1011
+ /**
1012
+ * Fetch a single WMTS tile. `mode` selects between KVP
1013
+ * (`?REQUEST=GetTile&...`) and the RESTful path
1014
+ * (`/{layer}/{style}/{tms}/{z}/{y}/{x}.{ext}`). honua-server
1015
+ * advertises both; the SDK defaults to RESTful because the wire path
1016
+ * is a single string substitution per tile and skips
1017
+ * URLSearchParams serialisation on the hot path.
1018
+ */
1019
+ async fetchWmtsTile(request) {
1020
+ return fetchWmtsTile(this.protocolTransport, request);
1021
+ }
1022
+ /**
1023
+ * WMTS GetFeatureInfo. honua-server accepts both KVP and RESTful
1024
+ * routing; mode default mirrors `fetchWmtsTile`.
1025
+ */
1026
+ async getWmtsFeatureInfo(request) {
1027
+ return getWmtsFeatureInfo(this.protocolTransport, request);
1028
+ }
1029
+ // ── OGC API Processes ───────────────────────────────────────
1030
+ async getOgcProcessesLanding(request = {}) {
1031
+ return getOgcProcessesLanding(this.protocolTransport, request);
1032
+ }
1033
+ async getOgcProcessesConformance(request = {}) {
1034
+ return getOgcProcessesConformance(this.protocolTransport, request);
1035
+ }
1036
+ async listOgcProcesses(request = {}) {
1037
+ return listOgcProcesses(this.protocolTransport, request);
1038
+ }
1039
+ async getOgcProcess(request) {
1040
+ return getOgcProcess(this.protocolTransport, request);
1041
+ }
1042
+ async executeOgcProcess(request) {
1043
+ return executeOgcProcess(this.protocolTransport, request);
1044
+ }
1045
+ async getOgcProcessJob(request) {
1046
+ return getOgcProcessJob(this.protocolTransport, request);
1047
+ }
1048
+ async getOgcProcessJobResults(request) {
1049
+ return getOgcProcessJobResults(this.protocolTransport, request);
1050
+ }
1051
+ async cancelOgcProcessJob(request) {
1052
+ return cancelOgcProcessJob(this.protocolTransport, request);
1053
+ }
1054
+ // ── STAC API ────────────────────────────────────────────────
1055
+ async getStacLanding(request = {}) {
1056
+ return getStacLanding(this.protocolTransport, request);
1057
+ }
1058
+ async listStacCollections(request = {}) {
1059
+ return listStacCollections(this.protocolTransport, request);
1060
+ }
1061
+ async getStacCollection(request) {
1062
+ return getStacCollection(this.protocolTransport, request);
1063
+ }
1064
+ async getStacItem(request) {
1065
+ return getStacItem(this.protocolTransport, request);
1066
+ }
1067
+ async searchStac(request = {}) {
1068
+ return searchStac(this.protocolTransport, request);
1069
+ }
1070
+ async getMapServiceMetadata(serviceId, options = {}) {
1071
+ return getMapServiceMetadata(this.protocolTransport, serviceId, options);
1072
+ }
1073
+ async getMapLayerMetadata(serviceId, layerId, options = {}) {
1074
+ return getMapLayerMetadata(this.protocolTransport, serviceId, layerId, options);
1075
+ }
1076
+ /**
1077
+ * Run a GeoServices `FeatureServer/query` request against a Honua-hosted layer.
1078
+ *
1079
+ * This is the canonical low-level read path. It maps directly to the FeatureServer
1080
+ * `query` endpoint and accepts the full ArcGIS query shape (`where`, `outFields`,
1081
+ * `geometry`, `spatialRel`, `orderByFields`, `resultRecordCount`, `outSr`, ...).
1082
+ *
1083
+ * For cross-protocol code (OGC, WFS, OData, STAC), prefer the protocol-neutral
1084
+ * {@link createDataset} contract — it normalizes capability differences and gives
1085
+ * you `Source.query(...)` plus paginated `Source.queryAll(...)` with explicit
1086
+ * `exceededTransferLimit` reporting.
1087
+ *
1088
+ * @example
1089
+ * ```ts
1090
+ * const { features, exceededTransferLimit } = await client.queryFeatures({
1091
+ * serviceId: "parcels",
1092
+ * layerId: 0,
1093
+ * where: "STATUS = 'ACTIVE'",
1094
+ * outFields: ["OBJECTID", "NAME"],
1095
+ * returnGeometry: true,
1096
+ * outSr: 4326,
1097
+ * resultRecordCount: 500,
1098
+ * });
1099
+ *
1100
+ * if (exceededTransferLimit) {
1101
+ * // re-issue with `resultOffset` or use queryFeaturesStream / dataset.source().queryAll()
1102
+ * }
1103
+ * ```
1104
+ */
1105
+ async queryFeatures(request) {
1106
+ if (this.transport === "grpc-web") {
1107
+ const client = await this.ensureConnectClient();
1108
+ const grpcAdapter = await HonuaClient.loadGrpcAdapter();
1109
+ const protoRequest = grpcAdapter.toProtoQueryRequest(request);
1110
+ try {
1111
+ const response = await client.queryFeatures(protoRequest, request.signal ? { signal: request.signal } : undefined);
1112
+ return grpcAdapter.fromProtoQueryResponse(response);
1113
+ }
1114
+ catch (error) {
1115
+ throw grpcAdapter.wrapConnectError(error);
1116
+ }
1117
+ }
1118
+ return queryFeaturesRest(this.protocolTransport, request, this.preferBinary);
1119
+ }
1120
+ async queryMapLayer(request) {
1121
+ return queryMapLayer(this.protocolTransport, request);
1122
+ }
1123
+ async applyEdits(request) {
1124
+ return applyEdits(this.protocolTransport, request);
1125
+ }
1126
+ async queryRelatedRecords(request) {
1127
+ return queryRelatedRecords(this.protocolTransport, request);
1128
+ }
1129
+ async queryMapRelatedRecords(request) {
1130
+ return queryMapRelatedRecords(this.protocolTransport, request);
1131
+ }
1132
+ async exportMap(request) {
1133
+ return exportMap(this.protocolTransport, request);
1134
+ }
1135
+ async getMapLegend(request) {
1136
+ return getMapLegend(this.protocolTransport, request);
1137
+ }
1138
+ async identifyMap(request) {
1139
+ return identifyMap(this.protocolTransport, request);
1140
+ }
1141
+ async findMap(request) {
1142
+ return findMap(this.protocolTransport, request);
1143
+ }
1144
+ async requestJson(method, path, init, callerSignal) {
1145
+ const request = {
1146
+ url: resolveRequestUrl(this.baseUrl, path),
1147
+ path,
1148
+ method,
1149
+ init: {
1150
+ method,
1151
+ headers: await this.composeHeaders({ Accept: "application/json" }, init?.headers),
1152
+ body: init?.body,
1153
+ },
1154
+ };
1155
+ return this.executeRequest(request, {
1156
+ callerSignal,
1157
+ finalize: async (response, durationMs, currentRequest, runAfter) => {
1158
+ // Parse the original body directly when no after-interceptor will read
1159
+ // it; only clone when an interceptor needs an independent copy.
1160
+ const body = await parseResponseBody(this.hasAfterInterceptors() ? response.clone() : response);
1161
+ // GeoServices/Esri services return HTTP 200 with a top-level `{error}`
1162
+ // envelope on failure (bad WHERE, invalid outFields, edit failures, …).
1163
+ // Surface these through the unified error model instead of handing the
1164
+ // failure envelope back as a "successful" response object.
1165
+ const envelopeError = geoServicesEnvelopeError(response.status, body);
1166
+ if (envelopeError) {
1167
+ await this.applyErrorInterceptors({
1168
+ request: cloneRequestContext(currentRequest),
1169
+ error: envelopeError,
1170
+ durationMs,
1171
+ });
1172
+ throw envelopeError;
1173
+ }
1174
+ await runAfter();
1175
+ return body;
1176
+ },
1177
+ });
1178
+ }
1179
+ /**
1180
+ * Request a PBF binary response and decode it. Falls back to JSON on failure.
1181
+ */
1182
+ async requestBinaryWithJsonFallback(method, path, params, callerSignal) {
1183
+ const request = {
1184
+ url: resolveRequestUrl(this.baseUrl, path),
1185
+ path,
1186
+ method,
1187
+ init: {
1188
+ method,
1189
+ headers: await this.composeHeaders({ Accept: "application/x-protobuf, application/json;q=0.9" }),
1190
+ },
1191
+ };
1192
+ return this.executeRequest(request, {
1193
+ callerSignal,
1194
+ finalize: async (response, durationMs, currentRequest, runAfter) => {
1195
+ await runAfter();
1196
+ // If the server returned PBF, decode it; on decode failure (including
1197
+ // Z/M geometry the fast path can't handle) retry as f=json.
1198
+ if (isPbfResponse(response)) {
1199
+ try {
1200
+ const buffer = await response.arrayBuffer();
1201
+ return decodePbfQueryResponse(buffer);
1202
+ }
1203
+ catch {
1204
+ params.set("f", "json");
1205
+ const jsonPath = `${stripQuery(path)}?${params.toString()}`;
1206
+ return this.requestJson("GET", jsonPath, undefined, callerSignal ?? currentRequest.init.signal ?? undefined);
1207
+ }
1208
+ }
1209
+ // Server returned JSON despite the PBF request (e.g. error or
1210
+ // unsupported). GeoServices reports query failures (bad WHERE, invalid
1211
+ // outFields, expired token => code 498/499) as HTTP 200 application/json
1212
+ // even when f=pbf was requested. Detect the `{error}` envelope here so
1213
+ // preferBinary callers throw a normalized HonuaHttpError exactly like
1214
+ // the JSON path instead of receiving the failure envelope as success.
1215
+ const body = await parseResponseBody(response);
1216
+ const envelopeError = geoServicesEnvelopeError(response.status, body);
1217
+ if (envelopeError) {
1218
+ await this.applyErrorInterceptors({
1219
+ request: cloneRequestContext(currentRequest),
1220
+ error: envelopeError,
1221
+ durationMs,
1222
+ });
1223
+ throw envelopeError;
1224
+ }
1225
+ return body;
1226
+ },
1227
+ });
1228
+ }
1229
+ /**
1230
+ * Fetch a text response (e.g. XML / JSON / plain) with an explicit Accept
1231
+ * negotiation. Used by the WFS adapter for `GetCapabilities`,
1232
+ * `Transaction` responses, and ExceptionReport bodies, and by the
1233
+ * WMS / WMTS Capabilities pipelines. Routes through the same
1234
+ * interceptor / retry / abort pipeline as `requestJson` /
1235
+ * `requestBytes`, so adapter callers do not need to bypass `HonuaClient`
1236
+ * to reach `fetch` directly.
1237
+ */
1238
+ async requestText(method, path, options) {
1239
+ const acceptHeader = options?.accept ?? "*/*";
1240
+ const headers = { Accept: acceptHeader };
1241
+ if (options?.contentType)
1242
+ headers["Content-Type"] = options.contentType;
1243
+ const request = {
1244
+ url: resolveRequestUrl(this.baseUrl, path),
1245
+ path,
1246
+ method,
1247
+ init: {
1248
+ method,
1249
+ headers: await this.composeHeaders(headers),
1250
+ ...(options?.body !== undefined ? { body: options.body } : {}),
1251
+ },
1252
+ };
1253
+ return this.executeRequest(request, {
1254
+ callerSignal: options?.signal,
1255
+ errorBody: async (response) => {
1256
+ const text = await response.clone().text();
1257
+ const contentType = response.headers.get("content-type") ?? acceptHeader;
1258
+ return text ? { raw: text, contentType } : {};
1259
+ },
1260
+ finalize: async (response, _durationMs, _request, runAfter) => {
1261
+ await runAfter();
1262
+ const text = await response.text();
1263
+ const contentType = response.headers.get("content-type") ?? acceptHeader;
1264
+ return { text, contentType, status: response.status };
1265
+ },
1266
+ });
1267
+ }
1268
+ /**
1269
+ * Fetch a binary response (raw bytes plus content type). Used by the
1270
+ * OGC API Tiles and OGC API Maps wire methods, both of which negotiate
1271
+ * non-JSON output formats. The interceptor / retry / abort plumbing
1272
+ * mirrors `requestJson`.
1273
+ */
1274
+ async requestBytes(method, path, accept, init, callerSignal) {
1275
+ const acceptHeader = accept ?? "application/octet-stream";
1276
+ const request = {
1277
+ url: resolveRequestUrl(this.baseUrl, path),
1278
+ path,
1279
+ method,
1280
+ init: {
1281
+ method,
1282
+ headers: await this.composeHeaders({ Accept: acceptHeader }, init?.headers),
1283
+ body: init?.body,
1284
+ },
1285
+ };
1286
+ return this.executeRequest(request, {
1287
+ callerSignal,
1288
+ finalize: async (response, _durationMs, _request, runAfter) => {
1289
+ await runAfter();
1290
+ const buffer = await response.arrayBuffer();
1291
+ const bytes = new Uint8Array(buffer);
1292
+ return {
1293
+ bytes,
1294
+ contentType: response.headers.get("content-type") ?? acceptHeader,
1295
+ empty: response.status === 204 || bytes.byteLength === 0,
1296
+ };
1297
+ },
1298
+ });
1299
+ }
1300
+ /**
1301
+ * Single owner of the HTTP attempt loop shared by every typed request
1302
+ * wrapper: before-interceptors, per-attempt timeout, network-error
1303
+ * normalization + retry, replay-safe auth refresh, HTTP-status retry, and the
1304
+ * after/error-interceptor calls. The previous implementation copy-pasted this
1305
+ * loop across six methods (pipelineFetch / requestCachedMetadataJson /
1306
+ * requestJson / requestBinaryWithJsonFallback / requestText / requestBytes),
1307
+ * and the copies had drifted in signal, okStatuses, and 304 handling — the
1308
+ * exact surface where an auth-replay / retry regression can appear in one
1309
+ * transport but not the others.
1310
+ *
1311
+ * Wrappers supply only what differs:
1312
+ * - `finalize` turns a successful `Response` into the wrapper's `T`. It is
1313
+ * handed a `runAfter` thunk so it controls *when* after-interceptors fire
1314
+ * (e.g. before vs. after a GeoServices error-envelope check) and can parse
1315
+ * the original body without a defensive clone (see `hasAfterInterceptors`).
1316
+ * - `shortCircuit` (optional) inspects the response before the ok/!ok split,
1317
+ * e.g. a metadata `304 Not Modified`.
1318
+ * - `okStatuses` (optional) treats specific non-2xx statuses as success.
1319
+ * - `errorBody` (optional) builds the body fed to `toHttpError` on failure.
1320
+ * - `onTerminalError` (optional) yields a fallback value instead of throwing
1321
+ * on a terminal network/HTTP error, e.g. stale-if-error metadata.
1322
+ */
1323
+ async executeRequest(initialRequest, options) {
1324
+ let request = await this.applyBeforeInterceptors(initialRequest);
1325
+ const retrySignal = options.callerSignal ?? request.init.signal ?? undefined;
1326
+ let refreshedAuth = false;
1327
+ for (let attempt = 0;; attempt += 1) {
1328
+ let response;
1329
+ const timeout = createTimeoutSignal(options.callerSignal ?? request.init.signal, this.timeoutMs);
1330
+ const startTime = performance.now();
1331
+ try {
1332
+ response = await this.fetchWithSafeRedirects(request.url, {
1333
+ ...request.init,
1334
+ method: request.method,
1335
+ signal: timeout.signal,
1336
+ });
1337
+ }
1338
+ catch (error) {
1339
+ const durationMs = performance.now() - startTime;
1340
+ const normalizedError = timeout.didTimeout
1341
+ ? new HonuaTimeoutError(this.timeoutMs ?? 0)
1342
+ : normalizeNetworkError(error);
1343
+ if (shouldRetryRequest(this.retryOptions, request.method, attempt, undefined, normalizedError)) {
1344
+ await this.sleepBeforeRetry(attempt, undefined, retrySignal);
1345
+ continue;
1346
+ }
1347
+ const fallback = options.onTerminalError?.(normalizedError);
1348
+ if (fallback !== undefined)
1349
+ return fallback;
1350
+ await this.applyErrorInterceptors({
1351
+ request: cloneRequestContext(request),
1352
+ error: normalizedError,
1353
+ durationMs,
1354
+ });
1355
+ throw normalizedError;
1356
+ }
1357
+ finally {
1358
+ timeout.dispose();
1359
+ }
1360
+ const durationMs = performance.now() - startTime;
1361
+ const currentRequest = request;
1362
+ const runAfter = async () => {
1363
+ try {
1364
+ await this.applyAfterInterceptors(cloneRequestContext(currentRequest), response, durationMs);
1365
+ }
1366
+ catch (error) {
1367
+ await this.applyErrorInterceptors({ request: cloneRequestContext(currentRequest), error, durationMs });
1368
+ throw error;
1369
+ }
1370
+ };
1371
+ if (options.shortCircuit) {
1372
+ const shorted = await options.shortCircuit(response, durationMs, currentRequest, runAfter);
1373
+ if (shorted)
1374
+ return shorted.value;
1375
+ }
1376
+ if (!response.ok && !options.okStatuses?.includes(response.status)) {
1377
+ const body = options.errorBody ? await options.errorBody(response) : await parseResponseBody(response.clone());
1378
+ const httpError = toHttpError(response.status, body);
1379
+ const authRefreshedRequest = refreshedAuth
1380
+ ? undefined
1381
+ : await this.refreshReplaySafeRequestAuth(request, response.status);
1382
+ if (authRefreshedRequest) {
1383
+ request = authRefreshedRequest;
1384
+ refreshedAuth = true;
1385
+ continue;
1386
+ }
1387
+ if (shouldRetryRequest(this.retryOptions, request.method, attempt, response.status, httpError)) {
1388
+ await this.sleepBeforeRetry(attempt, response, retrySignal);
1389
+ continue;
1390
+ }
1391
+ const fallback = options.onTerminalError?.(httpError);
1392
+ if (fallback !== undefined)
1393
+ return fallback;
1394
+ await this.applyErrorInterceptors({ request: cloneRequestContext(request), error: httpError, durationMs });
1395
+ throw httpError;
1396
+ }
1397
+ return options.finalize(response, durationMs, currentRequest, runAfter);
1398
+ }
1399
+ }
1400
+ async applyBeforeInterceptors(request) {
1401
+ let next = cloneRequestContext(request);
1402
+ next = {
1403
+ ...next,
1404
+ url: normalizeInterceptorRequestUrl(this.baseUrl, next.url),
1405
+ };
1406
+ for (const interceptor of this.interceptors) {
1407
+ const mutation = await interceptor.before?.(cloneRequestContext(next));
1408
+ if (!mutation) {
1409
+ continue;
1410
+ }
1411
+ next = applyRequestMutation(next, mutation);
1412
+ next = {
1413
+ ...next,
1414
+ url: normalizeInterceptorRequestUrl(this.baseUrl, next.url),
1415
+ };
1416
+ }
1417
+ return next;
1418
+ }
1419
+ async applyAfterInterceptors(request, response, durationMs) {
1420
+ for (const interceptor of this.interceptors) {
1421
+ // Only clone the response for interceptors that actually inspect it. This
1422
+ // keeps the no-after-interceptor hot path free of `Response.clone()` (so
1423
+ // typed wrappers can parse the original body directly) and avoids cloning
1424
+ // for `before`/`error`-only interceptors.
1425
+ if (!interceptor.after) {
1426
+ continue;
1427
+ }
1428
+ const context = {
1429
+ request: cloneRequestContext(request),
1430
+ response: response.clone(),
1431
+ durationMs,
1432
+ };
1433
+ await interceptor.after(context);
1434
+ }
1435
+ }
1436
+ /** Whether any registered interceptor inspects responses (`after` hook). */
1437
+ hasAfterInterceptors() {
1438
+ return this.interceptors.some((interceptor) => interceptor.after !== undefined);
1439
+ }
1440
+ async applyErrorInterceptors(context) {
1441
+ for (const interceptor of this.interceptors) {
1442
+ try {
1443
+ await interceptor.error?.(context);
1444
+ }
1445
+ catch {
1446
+ // Preserve original request failure; interceptor failures should not mask it.
1447
+ }
1448
+ }
1449
+ }
1450
+ async refreshReplaySafeRequestAuth(request, statusCode) {
1451
+ if ((statusCode !== 401 && statusCode !== 403) ||
1452
+ !this.authProvider ||
1453
+ !DEFAULT_RETRY_METHODS.has(request.method)) {
1454
+ return undefined;
1455
+ }
1456
+ const refreshed = await this.resolveAuthCredentials({ forceRefresh: true, reason: "unauthorized" });
1457
+ if (!refreshed)
1458
+ return undefined;
1459
+ return {
1460
+ ...request,
1461
+ init: {
1462
+ ...request.init,
1463
+ headers: mergeHeaders(request.init.headers, authHeadersFromCredentials(refreshed.credentials)),
1464
+ },
1465
+ };
1466
+ }
1467
+ async sleepBeforeRetry(attempt, response, signal) {
1468
+ await sleep(resolveRetryDelayMs(this.retryOptions, attempt, response), signal);
1469
+ }
1470
+ }
1471
+ /**
1472
+ * Detect a GeoServices/Esri error envelope returned on an HTTP 2xx response.
1473
+ *
1474
+ * GeoServices services (FeatureServer/MapServer/GeocodeServer/…) report
1475
+ * operation failures as HTTP 200 with a body of the shape
1476
+ * `{ error: { code, message, ... } }`. Without this guard such failures are
1477
+ * cast verbatim to the success response type and surface downstream as empty
1478
+ * results or confusing `undefined` access. Returns a normalized
1479
+ * {@link HonuaHttpError} when the envelope is present, otherwise `undefined`.
1480
+ */
1481
+ function geoServicesEnvelopeError(httpStatus, body) {
1482
+ if (!isObject(body) || !isObject(body.error)) {
1483
+ return undefined;
1484
+ }
1485
+ const error = body.error;
1486
+ const hasCode = typeof error.code === "number";
1487
+ const hasMessage = typeof error.message === "string";
1488
+ if (!hasCode && !hasMessage) {
1489
+ return undefined;
1490
+ }
1491
+ const statusCode = hasCode ? error.code : httpStatus;
1492
+ const message = hasMessage ? error.message : "Request failed";
1493
+ return new HonuaHttpError(statusCode, message, body);
1494
+ }
1495
+ async function parseResponseBody(response) {
1496
+ const text = await response.text();
1497
+ if (!text) {
1498
+ return {};
1499
+ }
1500
+ try {
1501
+ return JSON.parse(text);
1502
+ }
1503
+ catch {
1504
+ return { raw: text };
1505
+ }
1506
+ }
1507
+ function isObject(value) {
1508
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1509
+ }
1510
+ function applyRequestMutation(request, mutation) {
1511
+ const nextInit = mutation.init === undefined
1512
+ ? request.init
1513
+ : {
1514
+ ...request.init,
1515
+ ...mutation.init,
1516
+ headers: mergeHeaders(request.init.headers, mutation.init.headers),
1517
+ };
1518
+ return {
1519
+ url: mutation.url ?? request.url,
1520
+ path: request.path,
1521
+ method: mutation.method ?? request.method,
1522
+ init: {
1523
+ ...nextInit,
1524
+ method: mutation.method ?? request.method,
1525
+ },
1526
+ };
1527
+ }
1528
+ function cloneRequestContext(request) {
1529
+ return {
1530
+ ...request,
1531
+ init: {
1532
+ ...request.init,
1533
+ headers: cloneHeadersInit(request.init.headers),
1534
+ },
1535
+ };
1536
+ }
1537
+ function cloneHeadersInit(headers) {
1538
+ return mergeHeaders(headers);
1539
+ }
1540
+ function normalizeAuthProvider(auth) {
1541
+ if (!auth) {
1542
+ return undefined;
1543
+ }
1544
+ if (typeof auth === "function") {
1545
+ return { getCredentials: auth };
1546
+ }
1547
+ return {
1548
+ getCredentials: (context) => auth.getCredentials(context),
1549
+ ...(auth.revokeCredentials ? { revokeCredentials: auth.revokeCredentials.bind(auth) } : {}),
1550
+ };
1551
+ }
1552
+ function normalizeAuthRefreshSkewMs(value) {
1553
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1554
+ return DEFAULT_AUTH_REFRESH_SKEW_MS;
1555
+ }
1556
+ return Math.max(0, Math.trunc(value));
1557
+ }
1558
+ function normalizeAuthCredentials(value) {
1559
+ if (typeof value === "string") {
1560
+ return value.length > 0 ? { bearerToken: value } : undefined;
1561
+ }
1562
+ if (!value) {
1563
+ return undefined;
1564
+ }
1565
+ const credentials = {};
1566
+ if (typeof value.apiKey === "string" && value.apiKey.length > 0) {
1567
+ credentials.apiKey = value.apiKey;
1568
+ }
1569
+ if (typeof value.bearerToken === "string" && value.bearerToken.length > 0) {
1570
+ credentials.bearerToken = value.bearerToken;
1571
+ }
1572
+ if (typeof value.authorization === "string" && value.authorization.length > 0) {
1573
+ credentials.authorization = value.authorization;
1574
+ }
1575
+ if (value.expiresAt !== undefined) {
1576
+ credentials.expiresAt = value.expiresAt;
1577
+ }
1578
+ if (!credentials.apiKey && !credentials.bearerToken && !credentials.authorization) {
1579
+ return undefined;
1580
+ }
1581
+ return credentials;
1582
+ }
1583
+ function normalizeAuthExpiresAtMs(expiresAt) {
1584
+ if (expiresAt === undefined) {
1585
+ return undefined;
1586
+ }
1587
+ if (expiresAt instanceof Date) {
1588
+ return Number.isFinite(expiresAt.getTime()) ? expiresAt.getTime() : undefined;
1589
+ }
1590
+ if (typeof expiresAt === "number") {
1591
+ return Number.isFinite(expiresAt) ? expiresAt : undefined;
1592
+ }
1593
+ const parsed = Date.parse(expiresAt);
1594
+ return Number.isFinite(parsed) ? parsed : undefined;
1595
+ }
1596
+ function isAuthCredentialsExpiring(cached, skewMs) {
1597
+ if (cached.expiresAtMs === undefined) {
1598
+ return false;
1599
+ }
1600
+ return cached.expiresAtMs - Date.now() <= skewMs;
1601
+ }
1602
+ function resolveAuthRefreshReason(cached) {
1603
+ return cached ? "expired" : "initial";
1604
+ }
1605
+ function authHeadersFromCredentials(credentials) {
1606
+ const headers = {};
1607
+ if (credentials.apiKey) {
1608
+ headers["X-API-Key"] = credentials.apiKey;
1609
+ }
1610
+ if (credentials.authorization) {
1611
+ headers.Authorization = credentials.authorization;
1612
+ }
1613
+ else if (credentials.bearerToken) {
1614
+ headers.Authorization = `Bearer ${credentials.bearerToken}`;
1615
+ }
1616
+ return Object.keys(headers).length > 0 ? headers : undefined;
1617
+ }
1618
+ function createMetadataCacheState(entry, status, options) {
1619
+ return createHonuaCacheState({
1620
+ scope: "metadata",
1621
+ status,
1622
+ keyFingerprint: entry.keyFingerprint,
1623
+ ageMs: Math.max(0, options.now - entry.cachedAtMs),
1624
+ ...(options.ttlMs !== undefined ? { ttlMs: options.ttlMs } : {}),
1625
+ ...(options.staleIfErrorMs !== undefined ? { staleIfErrorMs: options.staleIfErrorMs } : {}),
1626
+ ...(options.revalidatedAt ? { revalidatedAt: options.revalidatedAt } : {}),
1627
+ ...(entry.sourceUpdatedAt ? { sourceUpdatedAt: entry.sourceUpdatedAt } : {}),
1628
+ ...(entry.validator ? { validator: entry.validator } : {}),
1629
+ ...(options.refreshErrorId ? { refreshErrorId: options.refreshErrorId } : {}),
1630
+ });
1631
+ }
1632
+ function metadataRefreshErrorId(error) {
1633
+ if (error instanceof HonuaHttpError) {
1634
+ return `http-${error.statusCode}`;
1635
+ }
1636
+ if (error instanceof HonuaTimeoutError) {
1637
+ return "timeout";
1638
+ }
1639
+ if (error instanceof HonuaNetworkError) {
1640
+ return "network";
1641
+ }
1642
+ if (error instanceof Error && error.name) {
1643
+ return error.name;
1644
+ }
1645
+ return "unknown";
1646
+ }
1647
+ function mergePathWithQueryParams(path, additionalParams) {
1648
+ if (additionalParams.size === 0) {
1649
+ return path;
1650
+ }
1651
+ const hashIndex = path.indexOf("#");
1652
+ const hash = hashIndex >= 0 ? path.slice(hashIndex) : "";
1653
+ const withoutHash = hashIndex >= 0 ? path.slice(0, hashIndex) : path;
1654
+ const queryIndex = withoutHash.indexOf("?");
1655
+ const basePath = queryIndex >= 0 ? withoutHash.slice(0, queryIndex) : withoutHash;
1656
+ const existingQuery = queryIndex >= 0 ? withoutHash.slice(queryIndex + 1) : "";
1657
+ const merged = new URLSearchParams(existingQuery);
1658
+ for (const [key, value] of additionalParams.entries()) {
1659
+ merged.set(key, value);
1660
+ }
1661
+ const nextQuery = merged.toString();
1662
+ const withQuery = nextQuery.length > 0 ? `${basePath}?${nextQuery}` : basePath;
1663
+ return `${withQuery}${hash}`;
1664
+ }