@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.
- package/LICENSE +201 -0
- package/README.md +9 -0
- package/contract/analytics-sources.d.ts +149 -0
- package/contract/analytics-sources.d.ts.map +1 -0
- package/contract/analytics-sources.js +235 -0
- package/contract/edit-session.d.ts +203 -0
- package/contract/edit-session.d.ts.map +1 -0
- package/contract/edit-session.js +760 -0
- package/contract/edit-sketch.d.ts +91 -0
- package/contract/edit-sketch.d.ts.map +1 -0
- package/contract/edit-sketch.js +281 -0
- package/contract/index.d.ts +61 -0
- package/contract/index.d.ts.map +1 -0
- package/contract/index.js +51 -0
- package/contract/jobs.d.ts +149 -0
- package/contract/jobs.d.ts.map +1 -0
- package/contract/jobs.js +40 -0
- package/contract/source.d.ts +423 -0
- package/contract/source.d.ts.map +1 -0
- package/contract/source.js +3802 -0
- package/contract/spatial-aggregation.d.ts +336 -0
- package/contract/spatial-aggregation.d.ts.map +1 -0
- package/contract/spatial-aggregation.js +298 -0
- package/contract/temporal.d.ts +455 -0
- package/contract/temporal.d.ts.map +1 -0
- package/contract/temporal.js +146 -0
- package/contract/tiles.d.ts +370 -0
- package/contract/tiles.d.ts.map +1 -0
- package/contract/tiles.js +519 -0
- package/contract/types.d.ts +665 -0
- package/contract/types.d.ts.map +1 -0
- package/contract/types.js +175 -0
- package/contract/widget-source.d.ts +205 -0
- package/contract/widget-source.d.ts.map +1 -0
- package/contract/widget-source.js +1473 -0
- package/control-plane/client.d.ts +95 -0
- package/control-plane/client.d.ts.map +1 -0
- package/control-plane/client.js +374 -0
- package/control-plane/index.d.ts +18 -0
- package/control-plane/index.d.ts.map +1 -0
- package/control-plane/index.js +14 -0
- package/control-plane/types.d.ts +233 -0
- package/control-plane/types.d.ts.map +1 -0
- package/control-plane/types.js +7 -0
- package/core/batch.d.ts +37 -0
- package/core/batch.d.ts.map +1 -0
- package/core/batch.js +79 -0
- package/core/cache-state.d.ts +79 -0
- package/core/cache-state.d.ts.map +1 -0
- package/core/cache-state.js +87 -0
- package/core/client.d.ts +577 -0
- package/core/client.d.ts.map +1 -0
- package/core/client.js +1664 -0
- package/core/compatibility.d.ts +18 -0
- package/core/compatibility.d.ts.map +1 -0
- package/core/compatibility.js +245 -0
- package/core/errors.d.ts +107 -0
- package/core/errors.d.ts.map +1 -0
- package/core/errors.js +156 -0
- package/core/esri-geojson.d.ts +66 -0
- package/core/esri-geojson.d.ts.map +1 -0
- package/core/esri-geojson.js +363 -0
- package/core/geoservices.d.ts +34 -0
- package/core/geoservices.d.ts.map +1 -0
- package/core/geoservices.js +434 -0
- package/core/grpc-adapter.d.ts +23 -0
- package/core/grpc-adapter.d.ts.map +1 -0
- package/core/grpc-adapter.js +688 -0
- package/core/odata.d.ts +347 -0
- package/core/odata.d.ts.map +1 -0
- package/core/odata.js +913 -0
- package/core/ogc-conformance.d.ts +35 -0
- package/core/ogc-conformance.d.ts.map +1 -0
- package/core/ogc-conformance.js +134 -0
- package/core/ogc-features.d.ts +23 -0
- package/core/ogc-features.d.ts.map +1 -0
- package/core/ogc-features.js +114 -0
- package/core/ogc-maps.d.ts +49 -0
- package/core/ogc-maps.d.ts.map +1 -0
- package/core/ogc-maps.js +126 -0
- package/core/ogc-processes.d.ts +118 -0
- package/core/ogc-processes.d.ts.map +1 -0
- package/core/ogc-processes.js +423 -0
- package/core/ogc-records.d.ts +75 -0
- package/core/ogc-records.d.ts.map +1 -0
- package/core/ogc-records.js +251 -0
- package/core/ogc-tiles.d.ts +125 -0
- package/core/ogc-tiles.d.ts.map +1 -0
- package/core/ogc-tiles.js +183 -0
- package/core/path-utils.d.ts +69 -0
- package/core/path-utils.d.ts.map +1 -0
- package/core/path-utils.js +123 -0
- package/core/pbf-decoder.d.ts +39 -0
- package/core/pbf-decoder.d.ts.map +1 -0
- package/core/pbf-decoder.js +702 -0
- package/core/process-runner.d.ts +104 -0
- package/core/process-runner.d.ts.map +1 -0
- package/core/process-runner.js +315 -0
- package/core/protocol-transport.d.ts +76 -0
- package/core/protocol-transport.d.ts.map +1 -0
- package/core/protocol-transport.js +15 -0
- package/core/query-builder.d.ts +261 -0
- package/core/query-builder.d.ts.map +1 -0
- package/core/query-builder.js +538 -0
- package/core/request-pipeline.d.ts +88 -0
- package/core/request-pipeline.d.ts.map +1 -0
- package/core/request-pipeline.js +273 -0
- package/core/spatial-filter.d.ts +93 -0
- package/core/spatial-filter.d.ts.map +1 -0
- package/core/spatial-filter.js +153 -0
- package/core/stac.d.ts +55 -0
- package/core/stac.d.ts.map +1 -0
- package/core/stac.js +229 -0
- package/core/surfaces.d.ts +527 -0
- package/core/surfaces.d.ts.map +1 -0
- package/core/surfaces.js +1917 -0
- package/core/terrain-elevation.d.ts +114 -0
- package/core/terrain-elevation.d.ts.map +1 -0
- package/core/terrain-elevation.js +150 -0
- package/core/types.d.ts +1355 -0
- package/core/types.d.ts.map +1 -0
- package/core/types.js +13 -0
- package/core/wfs-capabilities.d.ts +143 -0
- package/core/wfs-capabilities.d.ts.map +1 -0
- package/core/wfs-capabilities.js +478 -0
- package/core/wfs-filter.d.ts +118 -0
- package/core/wfs-filter.d.ts.map +1 -0
- package/core/wfs-filter.js +634 -0
- package/core/wfs.d.ts +219 -0
- package/core/wfs.d.ts.map +1 -0
- package/core/wfs.js +406 -0
- package/core/wire-shared.d.ts +32 -0
- package/core/wire-shared.d.ts.map +1 -0
- package/core/wire-shared.js +76 -0
- package/core/wms-axis.d.ts +36 -0
- package/core/wms-axis.d.ts.map +1 -0
- package/core/wms-axis.js +73 -0
- package/core/wms-capabilities.d.ts +110 -0
- package/core/wms-capabilities.d.ts.map +1 -0
- package/core/wms-capabilities.js +559 -0
- package/core/wms-types.d.ts +128 -0
- package/core/wms-types.d.ts.map +1 -0
- package/core/wms-types.js +32 -0
- package/core/wms.d.ts +144 -0
- package/core/wms.d.ts.map +1 -0
- package/core/wms.js +338 -0
- package/core/wmts-capabilities.d.ts +82 -0
- package/core/wmts-capabilities.d.ts.map +1 -0
- package/core/wmts-capabilities.js +402 -0
- package/core/wmts.d.ts +103 -0
- package/core/wmts.d.ts.map +1 -0
- package/core/wmts.js +332 -0
- package/core/xml-text.d.ts +20 -0
- package/core/xml-text.d.ts.map +1 -0
- package/core/xml-text.js +85 -0
- package/esri-compat/basemap-gallery.d.ts +38 -0
- package/esri-compat/basemap-gallery.d.ts.map +1 -0
- package/esri-compat/basemap-gallery.js +159 -0
- package/esri-compat/basemap-layer-list.d.ts +35 -0
- package/esri-compat/basemap-layer-list.d.ts.map +1 -0
- package/esri-compat/basemap-layer-list.js +139 -0
- package/esri-compat/basemap.d.ts +32 -0
- package/esri-compat/basemap.d.ts.map +1 -0
- package/esri-compat/basemap.js +84 -0
- package/esri-compat/bookmarks.d.ts +36 -0
- package/esri-compat/bookmarks.d.ts.map +1 -0
- package/esri-compat/bookmarks.js +108 -0
- package/esri-compat/class-breaks-renderer.d.ts +50 -0
- package/esri-compat/class-breaks-renderer.d.ts.map +1 -0
- package/esri-compat/class-breaks-renderer.js +164 -0
- package/esri-compat/color.d.ts +22 -0
- package/esri-compat/color.d.ts.map +1 -0
- package/esri-compat/color.js +106 -0
- package/esri-compat/controls.d.ts +174 -0
- package/esri-compat/controls.d.ts.map +1 -0
- package/esri-compat/controls.js +479 -0
- package/esri-compat/coordinate-conversion.d.ts +41 -0
- package/esri-compat/coordinate-conversion.d.ts.map +1 -0
- package/esri-compat/coordinate-conversion.js +125 -0
- package/esri-compat/directions.d.ts +45 -0
- package/esri-compat/directions.d.ts.map +1 -0
- package/esri-compat/directions.js +123 -0
- package/esri-compat/editor.d.ts +47 -0
- package/esri-compat/editor.d.ts.map +1 -0
- package/esri-compat/editor.js +170 -0
- package/esri-compat/esri-config.d.ts +16 -0
- package/esri-compat/esri-config.d.ts.map +1 -0
- package/esri-compat/esri-config.js +23 -0
- package/esri-compat/esri-request.d.ts +17 -0
- package/esri-compat/esri-request.d.ts.map +1 -0
- package/esri-compat/esri-request.js +127 -0
- package/esri-compat/event-bus.d.ts +106 -0
- package/esri-compat/event-bus.d.ts.map +1 -0
- package/esri-compat/event-bus.js +90 -0
- package/esri-compat/expand.d.ts +36 -0
- package/esri-compat/expand.d.ts.map +1 -0
- package/esri-compat/expand.js +97 -0
- package/esri-compat/extent.d.ts +43 -0
- package/esri-compat/extent.d.ts.map +1 -0
- package/esri-compat/extent.js +140 -0
- package/esri-compat/feature-filter.d.ts +55 -0
- package/esri-compat/feature-filter.d.ts.map +1 -0
- package/esri-compat/feature-filter.js +109 -0
- package/esri-compat/feature-form.d.ts +64 -0
- package/esri-compat/feature-form.d.ts.map +1 -0
- package/esri-compat/feature-form.js +137 -0
- package/esri-compat/feature-layer.d.ts +173 -0
- package/esri-compat/feature-layer.d.ts.map +1 -0
- package/esri-compat/feature-layer.js +661 -0
- package/esri-compat/feature-set.d.ts +32 -0
- package/esri-compat/feature-set.d.ts.map +1 -0
- package/esri-compat/feature-set.js +80 -0
- package/esri-compat/feature-table.d.ts +141 -0
- package/esri-compat/feature-table.d.ts.map +1 -0
- package/esri-compat/feature-table.js +500 -0
- package/esri-compat/feature-templates.d.ts +41 -0
- package/esri-compat/feature-templates.d.ts.map +1 -0
- package/esri-compat/feature-templates.js +97 -0
- package/esri-compat/feature.d.ts +33 -0
- package/esri-compat/feature.d.ts.map +1 -0
- package/esri-compat/feature.js +83 -0
- package/esri-compat/geojson-layer.d.ts +86 -0
- package/esri-compat/geojson-layer.d.ts.map +1 -0
- package/esri-compat/geojson-layer.js +191 -0
- package/esri-compat/geometry-engine.d.ts +107 -0
- package/esri-compat/geometry-engine.d.ts.map +1 -0
- package/esri-compat/geometry-engine.js +266 -0
- package/esri-compat/graphic.d.ts +46 -0
- package/esri-compat/graphic.d.ts.map +1 -0
- package/esri-compat/graphic.js +103 -0
- package/esri-compat/graphics-layer.d.ts +49 -0
- package/esri-compat/graphics-layer.d.ts.map +1 -0
- package/esri-compat/graphics-layer.js +185 -0
- package/esri-compat/group-layer.d.ts +48 -0
- package/esri-compat/group-layer.d.ts.map +1 -0
- package/esri-compat/group-layer.js +212 -0
- package/esri-compat/identify.d.ts +71 -0
- package/esri-compat/identify.d.ts.map +1 -0
- package/esri-compat/identify.js +299 -0
- package/esri-compat/identity-manager.d.ts +31 -0
- package/esri-compat/identity-manager.d.ts.map +1 -0
- package/esri-compat/identity-manager.js +115 -0
- package/esri-compat/imagery-layer.d.ts +78 -0
- package/esri-compat/imagery-layer.d.ts.map +1 -0
- package/esri-compat/imagery-layer.js +167 -0
- package/esri-compat/label-class.d.ts +31 -0
- package/esri-compat/label-class.d.ts.map +1 -0
- package/esri-compat/label-class.js +102 -0
- package/esri-compat/layer-list.d.ts +77 -0
- package/esri-compat/layer-list.d.ts.map +1 -0
- package/esri-compat/layer-list.js +342 -0
- package/esri-compat/legend.d.ts +53 -0
- package/esri-compat/legend.d.ts.map +1 -0
- package/esri-compat/legend.js +254 -0
- package/esri-compat/map-image-layer.d.ts +162 -0
- package/esri-compat/map-image-layer.d.ts.map +1 -0
- package/esri-compat/map-image-layer.js +709 -0
- package/esri-compat/map-view.d.ts +323 -0
- package/esri-compat/map-view.d.ts.map +1 -0
- package/esri-compat/map-view.js +1175 -0
- package/esri-compat/map.d.ts +47 -0
- package/esri-compat/map.d.ts.map +1 -0
- package/esri-compat/map.js +209 -0
- package/esri-compat/measurement-2d.d.ts +61 -0
- package/esri-compat/measurement-2d.d.ts.map +1 -0
- package/esri-compat/measurement-2d.js +184 -0
- package/esri-compat/measurement.d.ts +45 -0
- package/esri-compat/measurement.d.ts.map +1 -0
- package/esri-compat/measurement.js +193 -0
- package/esri-compat/oauth-info.d.ts +35 -0
- package/esri-compat/oauth-info.d.ts.map +1 -0
- package/esri-compat/oauth-info.js +115 -0
- package/esri-compat/picture-marker-symbol.d.ts +35 -0
- package/esri-compat/picture-marker-symbol.d.ts.map +1 -0
- package/esri-compat/picture-marker-symbol.js +126 -0
- package/esri-compat/point.d.ts +31 -0
- package/esri-compat/point.d.ts.map +1 -0
- package/esri-compat/point.js +102 -0
- package/esri-compat/polygon.d.ts +31 -0
- package/esri-compat/polygon.d.ts.map +1 -0
- package/esri-compat/polygon.js +107 -0
- package/esri-compat/polyline.d.ts +31 -0
- package/esri-compat/polyline.d.ts.map +1 -0
- package/esri-compat/polyline.js +107 -0
- package/esri-compat/popup-template.d.ts +55 -0
- package/esri-compat/popup-template.d.ts.map +1 -0
- package/esri-compat/popup-template.js +128 -0
- package/esri-compat/popup.d.ts +54 -0
- package/esri-compat/popup.d.ts.map +1 -0
- package/esri-compat/popup.js +312 -0
- package/esri-compat/print.d.ts +47 -0
- package/esri-compat/print.d.ts.map +1 -0
- package/esri-compat/print.js +105 -0
- package/esri-compat/query.d.ts +58 -0
- package/esri-compat/query.d.ts.map +1 -0
- package/esri-compat/query.js +128 -0
- package/esri-compat/reactive-utils.d.ts +24 -0
- package/esri-compat/reactive-utils.d.ts.map +1 -0
- package/esri-compat/reactive-utils.js +193 -0
- package/esri-compat/request.d.ts +41 -0
- package/esri-compat/request.d.ts.map +1 -0
- package/esri-compat/request.js +253 -0
- package/esri-compat/route-layer.d.ts +59 -0
- package/esri-compat/route-layer.d.ts.map +1 -0
- package/esri-compat/route-layer.js +223 -0
- package/esri-compat/route-task.d.ts +88 -0
- package/esri-compat/route-task.d.ts.map +1 -0
- package/esri-compat/route-task.js +225 -0
- package/esri-compat/scene-view.d.ts +41 -0
- package/esri-compat/scene-view.d.ts.map +1 -0
- package/esri-compat/scene-view.js +68 -0
- package/esri-compat/search.d.ts +112 -0
- package/esri-compat/search.d.ts.map +1 -0
- package/esri-compat/search.js +820 -0
- package/esri-compat/simple-fill-symbol.d.ts +27 -0
- package/esri-compat/simple-fill-symbol.d.ts.map +1 -0
- package/esri-compat/simple-fill-symbol.js +85 -0
- package/esri-compat/simple-line-symbol.d.ts +27 -0
- package/esri-compat/simple-line-symbol.d.ts.map +1 -0
- package/esri-compat/simple-line-symbol.js +85 -0
- package/esri-compat/simple-marker-symbol.d.ts +29 -0
- package/esri-compat/simple-marker-symbol.d.ts.map +1 -0
- package/esri-compat/simple-marker-symbol.js +92 -0
- package/esri-compat/simple-renderer.d.ts +29 -0
- package/esri-compat/simple-renderer.d.ts.map +1 -0
- package/esri-compat/simple-renderer.js +92 -0
- package/esri-compat/sketch.d.ts +64 -0
- package/esri-compat/sketch.d.ts.map +1 -0
- package/esri-compat/sketch.js +227 -0
- package/esri-compat/spatial-reference.d.ts +31 -0
- package/esri-compat/spatial-reference.d.ts.map +1 -0
- package/esri-compat/spatial-reference.js +102 -0
- package/esri-compat/swipe.d.ts +34 -0
- package/esri-compat/swipe.d.ts.map +1 -0
- package/esri-compat/swipe.js +93 -0
- package/esri-compat/table-list.d.ts +36 -0
- package/esri-compat/table-list.d.ts.map +1 -0
- package/esri-compat/table-list.js +126 -0
- package/esri-compat/text-symbol.d.ts +37 -0
- package/esri-compat/text-symbol.d.ts.map +1 -0
- package/esri-compat/text-symbol.js +123 -0
- package/esri-compat/tile-layer.d.ts +52 -0
- package/esri-compat/tile-layer.d.ts.map +1 -0
- package/esri-compat/tile-layer.js +168 -0
- package/esri-compat/time-slider.d.ts +70 -0
- package/esri-compat/time-slider.d.ts.map +1 -0
- package/esri-compat/time-slider.js +208 -0
- package/esri-compat/track.d.ts +51 -0
- package/esri-compat/track.d.ts.map +1 -0
- package/esri-compat/track.js +174 -0
- package/esri-compat/unique-value-renderer.d.ts +41 -0
- package/esri-compat/unique-value-renderer.d.ts.map +1 -0
- package/esri-compat/unique-value-renderer.js +127 -0
- package/esri-compat/url.d.ts +13 -0
- package/esri-compat/url.d.ts.map +1 -0
- package/esri-compat/url.js +68 -0
- package/esri-compat/vector-tile-layer.d.ts +47 -0
- package/esri-compat/vector-tile-layer.d.ts.map +1 -0
- package/esri-compat/vector-tile-layer.js +130 -0
- package/esri-compat/web-map.d.ts +14 -0
- package/esri-compat/web-map.d.ts.map +1 -0
- package/esri-compat/web-map.js +24 -0
- package/esri-compat/wfs-layer.d.ts +64 -0
- package/esri-compat/wfs-layer.d.ts.map +1 -0
- package/esri-compat/wfs-layer.js +145 -0
- package/esri-compat/wms-layer.d.ts +76 -0
- package/esri-compat/wms-layer.d.ts.map +1 -0
- package/esri-compat/wms-layer.js +157 -0
- package/exploration/context.d.ts +25 -0
- package/exploration/context.d.ts.map +1 -0
- package/exploration/context.js +396 -0
- package/exploration/index.d.ts +27 -0
- package/exploration/index.d.ts.map +1 -0
- package/exploration/index.js +24 -0
- package/exploration/presets.d.ts +27 -0
- package/exploration/presets.d.ts.map +1 -0
- package/exploration/presets.js +114 -0
- package/exploration/reducer.d.ts +23 -0
- package/exploration/reducer.d.ts.map +1 -0
- package/exploration/reducer.js +332 -0
- package/exploration/selection.d.ts +25 -0
- package/exploration/selection.d.ts.map +1 -0
- package/exploration/selection.js +34 -0
- package/exploration/selectors.d.ts +56 -0
- package/exploration/selectors.d.ts.map +1 -0
- package/exploration/selectors.js +110 -0
- package/exploration/types.d.ts +294 -0
- package/exploration/types.d.ts.map +1 -0
- package/exploration/types.js +33 -0
- package/expr/expression.d.ts +457 -0
- package/expr/expression.d.ts.map +1 -0
- package/expr/expression.js +663 -0
- package/expr/index.d.ts +32 -0
- package/expr/index.d.ts.map +1 -0
- package/expr/index.js +30 -0
- package/gen/honua/v1/feature_service_pb.d.ts +918 -0
- package/gen/honua/v1/feature_service_pb.d.ts.map +1 -0
- package/gen/honua/v1/feature_service_pb.js +375 -0
- package/generated-app/errors.d.ts +41 -0
- package/generated-app/errors.d.ts.map +1 -0
- package/generated-app/errors.js +41 -0
- package/generated-app/index.d.ts +17 -0
- package/generated-app/index.d.ts.map +1 -0
- package/generated-app/index.js +12 -0
- package/generated-app/manifest.d.ts +183 -0
- package/generated-app/manifest.d.ts.map +1 -0
- package/generated-app/manifest.js +14 -0
- package/generated-app/projection.d.ts +44 -0
- package/generated-app/projection.d.ts.map +1 -0
- package/generated-app/projection.js +234 -0
- package/generated-app/runtime.d.ts +215 -0
- package/generated-app/runtime.d.ts.map +1 -0
- package/generated-app/runtime.js +1196 -0
- package/interactions/detail-model.d.ts +54 -0
- package/interactions/detail-model.d.ts.map +1 -0
- package/interactions/detail-model.js +105 -0
- package/interactions/exploration-bindings.d.ts +120 -0
- package/interactions/exploration-bindings.d.ts.map +1 -0
- package/interactions/exploration-bindings.js +218 -0
- package/interactions/feature-state.d.ts +144 -0
- package/interactions/feature-state.d.ts.map +1 -0
- package/interactions/feature-state.js +169 -0
- package/interactions/hit-test.d.ts +112 -0
- package/interactions/hit-test.d.ts.map +1 -0
- package/interactions/hit-test.js +291 -0
- package/interactions/index.d.ts +31 -0
- package/interactions/index.d.ts.map +1 -0
- package/interactions/index.js +24 -0
- package/interactions/share-state.d.ts +36 -0
- package/interactions/share-state.d.ts.map +1 -0
- package/interactions/share-state.js +123 -0
- package/map/feature-service-adapter.d.ts +138 -0
- package/map/feature-service-adapter.d.ts.map +1 -0
- package/map/feature-service-adapter.js +279 -0
- package/map/honua-map.d.ts +203 -0
- package/map/honua-map.d.ts.map +1 -0
- package/map/honua-map.js +347 -0
- package/map/index.d.ts +27 -0
- package/map/index.d.ts.map +1 -0
- package/map/index.js +22 -0
- package/map/maplibre-target.d.ts +103 -0
- package/map/maplibre-target.d.ts.map +1 -0
- package/map/maplibre-target.js +164 -0
- package/map/webmap-maplibre.d.ts +60 -0
- package/map/webmap-maplibre.d.ts.map +1 -0
- package/map/webmap-maplibre.js +212 -0
- package/package.json +37 -0
- package/react/context.d.ts +32 -0
- package/react/context.d.ts.map +1 -0
- package/react/context.js +19 -0
- package/react/honua-layer.d.ts +35 -0
- package/react/honua-layer.d.ts.map +1 -0
- package/react/honua-layer.js +47 -0
- package/react/honua-map.d.ts +83 -0
- package/react/honua-map.d.ts.map +1 -0
- package/react/honua-map.js +109 -0
- package/react/honua-popup.d.ts +26 -0
- package/react/honua-popup.d.ts.map +1 -0
- package/react/honua-popup.js +34 -0
- package/react/hooks.d.ts +127 -0
- package/react/hooks.d.ts.map +1 -0
- package/react/hooks.js +206 -0
- package/react/index.d.ts +42 -0
- package/react/index.d.ts.map +1 -0
- package/react/index.js +34 -0
- package/react/provider.d.ts +36 -0
- package/react/provider.d.ts.map +1 -0
- package/react/provider.js +35 -0
- package/react/query-cache.d.ts +76 -0
- package/react/query-cache.d.ts.map +1 -0
- package/react/query-cache.js +188 -0
- package/runtime/diff.d.ts +31 -0
- package/runtime/diff.d.ts.map +1 -0
- package/runtime/diff.js +143 -0
- package/runtime/errors.d.ts +40 -0
- package/runtime/errors.d.ts.map +1 -0
- package/runtime/errors.js +35 -0
- package/runtime/index.d.ts +59 -0
- package/runtime/index.d.ts.map +1 -0
- package/runtime/index.js +43 -0
- package/runtime/legend.d.ts +23 -0
- package/runtime/legend.d.ts.map +1 -0
- package/runtime/legend.js +38 -0
- package/runtime/load-package.d.ts +125 -0
- package/runtime/load-package.d.ts.map +1 -0
- package/runtime/load-package.js +264 -0
- package/runtime/map-package-fetch.d.ts +107 -0
- package/runtime/map-package-fetch.d.ts.map +1 -0
- package/runtime/map-package-fetch.js +336 -0
- package/runtime/map-package-realtime.d.ts +169 -0
- package/runtime/map-package-realtime.d.ts.map +1 -0
- package/runtime/map-package-realtime.js +350 -0
- package/runtime/map-package-validation.d.ts +43 -0
- package/runtime/map-package-validation.d.ts.map +1 -0
- package/runtime/map-package-validation.js +269 -0
- package/runtime/map-package-watch.d.ts +78 -0
- package/runtime/map-package-watch.d.ts.map +1 -0
- package/runtime/map-package-watch.js +695 -0
- package/runtime/map-package.d.ts +167 -0
- package/runtime/map-package.d.ts.map +1 -0
- package/runtime/map-package.js +15 -0
- package/runtime/ogc-styles.d.ts +110 -0
- package/runtime/ogc-styles.d.ts.map +1 -0
- package/runtime/ogc-styles.js +195 -0
- package/runtime/popups.d.ts +62 -0
- package/runtime/popups.d.ts.map +1 -0
- package/runtime/popups.js +95 -0
- package/runtime/query-tiles.d.ts +217 -0
- package/runtime/query-tiles.d.ts.map +1 -0
- package/runtime/query-tiles.js +801 -0
- package/runtime/runtime.d.ts +243 -0
- package/runtime/runtime.d.ts.map +1 -0
- package/runtime/runtime.js +1147 -0
- package/runtime/source-bridge.d.ts +99 -0
- package/runtime/source-bridge.d.ts.map +1 -0
- package/runtime/source-bridge.js +401 -0
- package/runtime/style-compose.d.ts +46 -0
- package/runtime/style-compose.d.ts.map +1 -0
- package/runtime/style-compose.js +144 -0
- package/runtime/style-interactions.d.ts +141 -0
- package/runtime/style-interactions.d.ts.map +1 -0
- package/runtime/style-interactions.js +574 -0
- package/runtime/style-spec-validation.d.ts +23 -0
- package/runtime/style-spec-validation.d.ts.map +1 -0
- package/runtime/style-spec-validation.js +357 -0
- package/studio/capability-manifest.d.ts +285 -0
- package/studio/capability-manifest.d.ts.map +1 -0
- package/studio/capability-manifest.js +59 -0
- package/studio/chart-spec.d.ts +85 -0
- package/studio/chart-spec.d.ts.map +1 -0
- package/studio/chart-spec.js +106 -0
- package/studio/index.d.ts +34 -0
- package/studio/index.d.ts.map +1 -0
- package/studio/index.js +27 -0
- package/studio/provenance.d.ts +131 -0
- package/studio/provenance.d.ts.map +1 -0
- package/studio/provenance.js +39 -0
- package/studio/publish.d.ts +64 -0
- package/studio/publish.d.ts.map +1 -0
- package/studio/publish.js +13 -0
- package/studio/types.d.ts +278 -0
- package/studio/types.d.ts.map +1 -0
- package/studio/types.js +71 -0
- package/studio/validate.d.ts +45 -0
- package/studio/validate.d.ts.map +1 -0
- package/studio/validate.js +255 -0
- package/studio/validation.d.ts +112 -0
- package/studio/validation.d.ts.map +1 -0
- package/studio/validation.js +42 -0
- package/style/factory.d.ts +30 -0
- package/style/factory.d.ts.map +1 -0
- package/style/factory.js +61 -0
- package/style/index.d.ts +25 -0
- package/style/index.d.ts.map +1 -0
- package/style/index.js +24 -0
- package/style/specification.d.ts +203 -0
- package/style/specification.d.ts.map +1 -0
- package/style/specification.js +178 -0
- package/style/wms-wmts-resolvers.d.ts +38 -0
- package/style/wms-wmts-resolvers.d.ts.map +1 -0
- package/style/wms-wmts-resolvers.js +92 -0
- package/webmap/contract.d.ts +28 -0
- package/webmap/contract.d.ts.map +1 -0
- package/webmap/contract.js +130 -0
- package/webmap/convert-basemap.d.ts +17 -0
- package/webmap/convert-basemap.d.ts.map +1 -0
- package/webmap/convert-basemap.js +78 -0
- package/webmap/convert-extent.d.ts +17 -0
- package/webmap/convert-extent.d.ts.map +1 -0
- package/webmap/convert-extent.js +114 -0
- package/webmap/convert-label.d.ts +13 -0
- package/webmap/convert-label.d.ts.map +1 -0
- package/webmap/convert-label.js +125 -0
- package/webmap/convert-layer.d.ts +22 -0
- package/webmap/convert-layer.d.ts.map +1 -0
- package/webmap/convert-layer.js +181 -0
- package/webmap/convert-popup.d.ts +34 -0
- package/webmap/convert-popup.d.ts.map +1 -0
- package/webmap/convert-popup.js +68 -0
- package/webmap/convert-renderer.d.ts +17 -0
- package/webmap/convert-renderer.d.ts.map +1 -0
- package/webmap/convert-renderer.js +161 -0
- package/webmap/convert-symbol.d.ts +17 -0
- package/webmap/convert-symbol.d.ts.map +1 -0
- package/webmap/convert-symbol.js +122 -0
- package/webmap/index.d.ts +44 -0
- package/webmap/index.d.ts.map +1 -0
- package/webmap/index.js +33 -0
- package/webmap/parse.d.ts +25 -0
- package/webmap/parse.d.ts.map +1 -0
- package/webmap/parse.js +83 -0
- package/webmap/types.d.ts +264 -0
- package/webmap/types.d.ts.map +1 -0
- package/webmap/types.js +13 -0
- package/webmap/warnings.d.ts +30 -0
- package/webmap/warnings.d.ts.map +1 -0
- 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
|
+
}
|