@honua/react 0.1.1-beta.0 → 0.1.4-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/contract/discovery.d.ts +34 -0
  2. package/contract/discovery.d.ts.map +1 -1
  3. package/contract/index.d.ts +7 -5
  4. package/contract/index.d.ts.map +1 -1
  5. package/contract/index.js +3 -2
  6. package/contract/pmtiles.d.ts +10 -1
  7. package/contract/pmtiles.d.ts.map +1 -1
  8. package/contract/pmtiles.js +10 -2
  9. package/contract/protocol-module.d.ts +84 -9
  10. package/contract/protocol-module.d.ts.map +1 -1
  11. package/contract/query-filter-fes.d.ts +18 -0
  12. package/contract/query-filter-fes.d.ts.map +1 -0
  13. package/contract/query-filter-fes.js +122 -0
  14. package/contract/query-filter.d.ts +300 -0
  15. package/contract/query-filter.d.ts.map +1 -0
  16. package/contract/query-filter.js +883 -0
  17. package/contract/schema.d.ts +12 -0
  18. package/contract/schema.d.ts.map +1 -1
  19. package/contract/schema.js +34 -0
  20. package/contract/source-capability-support.d.ts.map +1 -1
  21. package/contract/source-capability-support.js +15 -3
  22. package/contract/source.d.ts +45 -25
  23. package/contract/source.d.ts.map +1 -1
  24. package/contract/source.js +719 -479
  25. package/contract/types.d.ts +87 -18
  26. package/contract/types.d.ts.map +1 -1
  27. package/contract/widget-source.d.ts.map +1 -1
  28. package/core/client.d.ts +45 -21
  29. package/core/client.d.ts.map +1 -1
  30. package/core/client.js +224 -46
  31. package/core/error-classifications.d.ts +6 -0
  32. package/core/error-classifications.d.ts.map +1 -1
  33. package/core/error-classifications.js +6 -0
  34. package/core/error-code-registry.d.ts +6 -0
  35. package/core/error-code-registry.d.ts.map +1 -1
  36. package/core/error-code-registry.js +6 -0
  37. package/core/error-envelope.js +2 -0
  38. package/core/errors.d.ts +1 -1
  39. package/core/errors.js +1 -1
  40. package/core/geoparquet-sql.d.ts +68 -4
  41. package/core/geoparquet-sql.d.ts.map +1 -1
  42. package/core/geoparquet-sql.js +325 -8
  43. package/core/odata-write-codec.d.ts.map +1 -1
  44. package/core/odata-write-codec.js +0 -1
  45. package/core/odata.d.ts +6 -0
  46. package/core/odata.d.ts.map +1 -1
  47. package/core/odata.js +6 -1
  48. package/core/ogc-processes.d.ts +182 -25
  49. package/core/ogc-processes.d.ts.map +1 -1
  50. package/core/ogc-processes.js +602 -30
  51. package/core/process-runner.d.ts +8 -1
  52. package/core/process-runner.d.ts.map +1 -1
  53. package/core/process-runner.js +1 -0
  54. package/core/protocol-transport.d.ts +2 -0
  55. package/core/protocol-transport.d.ts.map +1 -1
  56. package/core/stac.d.ts +31 -0
  57. package/core/stac.d.ts.map +1 -1
  58. package/core/stac.js +136 -19
  59. package/core/surfaces.d.ts.map +1 -1
  60. package/core/surfaces.js +273 -68
  61. package/core/types.d.ts +123 -9
  62. package/core/types.d.ts.map +1 -1
  63. package/core/wfs-axis-order.d.ts +18 -0
  64. package/core/wfs-axis-order.d.ts.map +1 -0
  65. package/core/wfs-axis-order.js +108 -0
  66. package/core/wfs-capabilities.d.ts +65 -24
  67. package/core/wfs-capabilities.d.ts.map +1 -1
  68. package/core/wfs-capabilities.js +237 -27
  69. package/core/wfs-filter.d.ts +26 -3
  70. package/core/wfs-filter.d.ts.map +1 -1
  71. package/core/wfs-filter.js +48 -53
  72. package/core/wfs-protocol-error.d.ts +12 -0
  73. package/core/wfs-protocol-error.d.ts.map +1 -0
  74. package/core/wfs-protocol-error.js +13 -0
  75. package/core/wfs.d.ts +53 -1
  76. package/core/wfs.d.ts.map +1 -1
  77. package/core/wfs.js +251 -29
  78. package/core/wms-feature-info.d.ts +92 -0
  79. package/core/wms-feature-info.d.ts.map +1 -0
  80. package/core/wms-feature-info.js +285 -0
  81. package/core/wms.d.ts +9 -0
  82. package/core/wms.d.ts.map +1 -1
  83. package/core/wms.js +17 -5
  84. package/esri-compat/event-bus.d.ts +20 -0
  85. package/esri-compat/event-bus.d.ts.map +1 -1
  86. package/esri-compat/feature-filter.d.ts +46 -1
  87. package/esri-compat/feature-filter.d.ts.map +1 -1
  88. package/esri-compat/feature-filter.js +49 -0
  89. package/esri-compat/geojson-layer.d.ts +4 -2
  90. package/esri-compat/geojson-layer.d.ts.map +1 -1
  91. package/esri-compat/geojson-layer.js +4 -2
  92. package/esri-compat/identity-manager.js +2 -2
  93. package/esri-compat/imagery-layer.d.ts +2 -1
  94. package/esri-compat/imagery-layer.d.ts.map +1 -1
  95. package/esri-compat/imagery-layer.js +2 -1
  96. package/esri-compat/layer-list.d.ts +8 -1
  97. package/esri-compat/layer-list.d.ts.map +1 -1
  98. package/esri-compat/layer-list.js +11 -4
  99. package/esri-compat/legend.d.ts.map +1 -1
  100. package/esri-compat/legend.js +1 -1
  101. package/esri-compat/locator.d.ts +297 -0
  102. package/esri-compat/locator.d.ts.map +1 -0
  103. package/esri-compat/locator.js +579 -0
  104. package/esri-compat/map-image-layer.d.ts.map +1 -1
  105. package/esri-compat/map-view.d.ts.map +1 -1
  106. package/esri-compat/map-view.js +1 -1
  107. package/esri-compat/scene-view.d.ts.map +1 -1
  108. package/esri-compat/time-slider.d.ts +29 -0
  109. package/esri-compat/time-slider.d.ts.map +1 -1
  110. package/esri-compat/time-slider.js +117 -0
  111. package/esri-compat/wfs-layer.d.ts +2 -1
  112. package/esri-compat/wfs-layer.d.ts.map +1 -1
  113. package/esri-compat/wfs-layer.js +2 -1
  114. package/esri-compat/widget-host.d.ts +42 -6
  115. package/esri-compat/widget-host.d.ts.map +1 -1
  116. package/esri-compat/widget-host.js +99 -10
  117. package/generated-app/projection.d.ts.map +1 -1
  118. package/generated-app/runtime.d.ts.map +1 -1
  119. package/map/data-to-map-bridge.d.ts +1 -1
  120. package/map/data-to-map-bridge.d.ts.map +1 -1
  121. package/map/data-to-map-bridge.js +1 -2
  122. package/map/honua-map.d.ts.map +1 -1
  123. package/map/honua-map.js +1 -1
  124. package/map/index.d.ts +2 -0
  125. package/map/index.d.ts.map +1 -1
  126. package/map/index.js +2 -0
  127. package/map/realtime-reconciliation-adapter.d.ts +98 -0
  128. package/map/realtime-reconciliation-adapter.d.ts.map +1 -0
  129. package/map/realtime-reconciliation-adapter.js +84 -0
  130. package/map/temporal-playback.d.ts +25 -0
  131. package/map/temporal-playback.d.ts.map +1 -1
  132. package/map/temporal-playback.js +29 -1
  133. package/package.json +4 -3
  134. package/query-planner/canonical-filter.d.ts +30 -0
  135. package/query-planner/canonical-filter.d.ts.map +1 -0
  136. package/query-planner/canonical-filter.js +30 -0
  137. package/query-planner/columnar-plan.d.ts +63 -0
  138. package/query-planner/columnar-plan.d.ts.map +1 -0
  139. package/query-planner/columnar-plan.js +94 -0
  140. package/query-planner/diagnostics.d.ts +3 -3
  141. package/query-planner/diagnostics.d.ts.map +1 -1
  142. package/query-planner/diagnostics.js +17 -4
  143. package/query-planner/duckdb.d.ts.map +1 -1
  144. package/query-planner/duckdb.js +16 -2
  145. package/query-planner/execution-context.d.ts.map +1 -1
  146. package/query-planner/execution-context.js +3 -1
  147. package/query-planner/executor.d.ts.map +1 -1
  148. package/query-planner/executor.js +7 -1
  149. package/query-planner/geoservices.d.ts.map +1 -1
  150. package/query-planner/geoservices.js +24 -5
  151. package/query-planner/grpc.d.ts.map +1 -1
  152. package/query-planner/grpc.js +29 -5
  153. package/query-planner/index.d.ts +20 -5
  154. package/query-planner/index.d.ts.map +1 -1
  155. package/query-planner/index.js +16 -3
  156. package/query-planner/ir.d.ts +6 -1
  157. package/query-planner/ir.d.ts.map +1 -1
  158. package/query-planner/ir.js +63 -1
  159. package/query-planner/odata-protocol-module.d.ts +54 -0
  160. package/query-planner/odata-protocol-module.d.ts.map +1 -0
  161. package/query-planner/odata-protocol-module.js +199 -0
  162. package/query-planner/odata-v1.d.ts +23 -0
  163. package/query-planner/odata-v1.d.ts.map +1 -0
  164. package/query-planner/odata-v1.js +202 -0
  165. package/query-planner/odata.d.ts +0 -3
  166. package/query-planner/odata.d.ts.map +1 -1
  167. package/query-planner/odata.js +0 -143
  168. package/query-planner/ogc-features.d.ts.map +1 -1
  169. package/query-planner/ogc-features.js +23 -1
  170. package/query-planner/planner.d.ts +2 -2
  171. package/query-planner/planner.d.ts.map +1 -1
  172. package/query-planner/planner.js +146 -27
  173. package/query-planner/representation.d.ts +59 -0
  174. package/query-planner/representation.d.ts.map +1 -0
  175. package/query-planner/representation.js +140 -0
  176. package/query-planner/types.d.ts +153 -3
  177. package/query-planner/types.d.ts.map +1 -1
  178. package/query-planner/wfs-protocol-module.d.ts +57 -0
  179. package/query-planner/wfs-protocol-module.d.ts.map +1 -0
  180. package/query-planner/wfs-protocol-module.js +731 -0
  181. package/query-planner/wfs-v1.d.ts +41 -0
  182. package/query-planner/wfs-v1.d.ts.map +1 -0
  183. package/query-planner/wfs-v1.js +366 -0
  184. package/query-planner/wfs.d.ts +1 -3
  185. package/query-planner/wfs.d.ts.map +1 -1
  186. package/query-planner/wfs.js +1 -70
  187. package/react/honua-map.d.ts +3 -1
  188. package/react/honua-map.d.ts.map +1 -1
  189. package/react/honua-map.js +11 -1
  190. package/react/hooks.d.ts +1 -1
  191. package/react/hooks.js +1 -1
  192. package/react/selection.d.ts.map +1 -1
  193. package/react/selection.js +0 -0
  194. package/react/use-mounted-source.d.ts +1 -1
  195. package/react/use-mounted-source.js +1 -1
  196. package/realtime/checkpoint-store.d.ts +206 -0
  197. package/realtime/checkpoint-store.d.ts.map +1 -0
  198. package/realtime/checkpoint-store.js +504 -0
  199. package/realtime/contract.d.ts +142 -0
  200. package/realtime/contract.d.ts.map +1 -0
  201. package/realtime/contract.js +154 -0
  202. package/realtime/exploration.d.ts +16 -0
  203. package/realtime/exploration.d.ts.map +1 -0
  204. package/realtime/exploration.js +32 -0
  205. package/realtime/honua-server.d.ts +131 -0
  206. package/realtime/honua-server.d.ts.map +1 -0
  207. package/realtime/honua-server.js +302 -0
  208. package/realtime/index.d.ts +39 -0
  209. package/realtime/index.d.ts.map +1 -0
  210. package/realtime/index.js +26 -0
  211. package/realtime/odata-delta.d.ts +114 -0
  212. package/realtime/odata-delta.d.ts.map +1 -0
  213. package/realtime/odata-delta.js +483 -0
  214. package/realtime/projections.d.ts +30 -0
  215. package/realtime/projections.d.ts.map +1 -0
  216. package/realtime/projections.js +51 -0
  217. package/realtime/reconciliation.d.ts +245 -0
  218. package/realtime/reconciliation.d.ts.map +1 -0
  219. package/realtime/reconciliation.js +377 -0
  220. package/realtime/reducer.d.ts +14 -0
  221. package/realtime/reducer.d.ts.map +1 -0
  222. package/realtime/reducer.js +351 -0
  223. package/realtime/resumable-transport.d.ts +111 -0
  224. package/realtime/resumable-transport.d.ts.map +1 -0
  225. package/realtime/resumable-transport.js +406 -0
  226. package/realtime/resumable.d.ts +104 -0
  227. package/realtime/resumable.d.ts.map +1 -0
  228. package/realtime/resumable.js +632 -0
  229. package/realtime/sse.d.ts +23 -0
  230. package/realtime/sse.d.ts.map +1 -0
  231. package/realtime/sse.js +147 -0
  232. package/realtime/store.d.ts +18 -0
  233. package/realtime/store.d.ts.map +1 -0
  234. package/realtime/store.js +63 -0
  235. package/realtime/types.d.ts +190 -0
  236. package/realtime/types.d.ts.map +1 -0
  237. package/realtime/types.js +10 -0
  238. package/realtime/websocket.d.ts +60 -0
  239. package/realtime/websocket.d.ts.map +1 -0
  240. package/realtime/websocket.js +157 -0
  241. package/realtime/wire.d.ts +28 -0
  242. package/realtime/wire.d.ts.map +1 -0
  243. package/realtime/wire.js +73 -0
  244. package/runtime/index.d.ts +6 -2
  245. package/runtime/index.d.ts.map +1 -1
  246. package/runtime/index.js +3 -1
  247. package/runtime/load-package.d.ts +24 -0
  248. package/runtime/load-package.d.ts.map +1 -1
  249. package/runtime/load-package.js +45 -1
  250. package/runtime/maplibre-renderer.d.ts +11 -0
  251. package/runtime/maplibre-renderer.d.ts.map +1 -1
  252. package/runtime/maplibre-renderer.js +32 -7
  253. package/runtime/offline-region-protocol.d.ts +73 -0
  254. package/runtime/offline-region-protocol.d.ts.map +1 -0
  255. package/runtime/offline-region-protocol.js +63 -0
  256. package/runtime/offline-region-style.d.ts +78 -0
  257. package/runtime/offline-region-style.d.ts.map +1 -0
  258. package/runtime/offline-region-style.js +147 -0
  259. package/runtime/pmtiles-protocol.d.ts +7 -8
  260. package/runtime/pmtiles-protocol.d.ts.map +1 -1
  261. package/runtime/pmtiles-protocol.js +19 -66
  262. package/runtime/protocol-registry.d.ts +55 -0
  263. package/runtime/protocol-registry.d.ts.map +1 -0
  264. package/runtime/protocol-registry.js +103 -0
  265. package/runtime/runtime.d.ts +2 -0
  266. package/runtime/runtime.d.ts.map +1 -1
  267. package/runtime/runtime.js +6 -1
  268. package/studio/index.d.ts +24 -7
  269. package/studio/index.d.ts.map +1 -1
  270. package/studio/index.js +21 -7
  271. package/studio/lifecycle-client.d.ts +164 -0
  272. package/studio/lifecycle-client.d.ts.map +1 -0
  273. package/studio/lifecycle-client.js +299 -0
  274. package/studio/lifecycle-errors.d.ts +91 -0
  275. package/studio/lifecycle-errors.d.ts.map +1 -0
  276. package/studio/lifecycle-errors.js +77 -0
  277. package/studio/lifecycle-types.d.ts +321 -0
  278. package/studio/lifecycle-types.d.ts.map +1 -0
  279. package/studio/lifecycle-types.js +22 -0
  280. package/webmap/convert-label.js +0 -1
  281. package/webmap/convert-symbol.d.ts.map +1 -1
package/core/surfaces.js CHANGED
@@ -774,33 +774,15 @@ export class HonuaOgcFeatures {
774
774
  return this.client.listOgcItems({ ...request, ...(layout ? { layout } : {}) });
775
775
  }
776
776
  async itemsAll(request) {
777
- const pageSize = normalizePageSize(request.pageSize, request.limit);
778
- const maxPages = normalizeMaxPages(request.maxPages);
779
- const offset = normalizeOffset(request.offset);
780
777
  const totalLimit = normalizeTotalLimit(request.limit);
781
778
  const features = [];
782
- for (let page = 0; page < maxPages; page += 1) {
783
- if (totalLimit !== undefined && features.length >= totalLimit) {
784
- break;
785
- }
786
- const remainingLimit = totalLimit === undefined ? pageSize : Math.max(0, totalLimit - features.length);
787
- if (remainingLimit < 1) {
788
- break;
789
- }
790
- const limit = Math.min(pageSize, remainingLimit);
791
- const response = await this.items({
792
- ...request,
793
- limit,
794
- offset: offset + page * pageSize,
795
- });
796
- const pageFeatures = extractOgcFeatures(response);
797
- if (pageFeatures.length === 0) {
798
- break;
799
- }
800
- features.push(...pageFeatures);
801
- if (pageFeatures.length < limit) {
802
- break;
803
- }
779
+ for await (const page of drainOgcItemPages((pageRequest) => this.items(withOgcItemsPage(request, pageRequest)), {
780
+ pageSize: normalizePageSize(request.pageSize, request.limit),
781
+ maxPages: normalizeMaxPages(request.maxPages),
782
+ startOffset: normalizeOffset(request.offset),
783
+ ...(totalLimit !== undefined ? { totalLimit } : {}),
784
+ })) {
785
+ features.push(...page);
804
786
  }
805
787
  if (totalLimit !== undefined && features.length > totalLimit) {
806
788
  return features.slice(0, totalLimit);
@@ -865,33 +847,15 @@ export class HonuaOgcFeatureCollection {
865
847
  });
866
848
  }
867
849
  async itemsAll(request = {}) {
868
- const pageSize = normalizePageSize(request.pageSize, request.limit);
869
- const maxPages = normalizeMaxPages(request.maxPages);
870
- const offset = normalizeOffset(request.offset);
871
850
  const totalLimit = normalizeTotalLimit(request.limit);
872
851
  const features = [];
873
- for (let page = 0; page < maxPages; page += 1) {
874
- if (totalLimit !== undefined && features.length >= totalLimit) {
875
- break;
876
- }
877
- const remainingLimit = totalLimit === undefined ? pageSize : Math.max(0, totalLimit - features.length);
878
- if (remainingLimit < 1) {
879
- break;
880
- }
881
- const limit = Math.min(pageSize, remainingLimit);
882
- const response = await this.items({
883
- ...request,
884
- limit,
885
- offset: offset + page * pageSize,
886
- });
887
- const pageFeatures = extractOgcFeatures(response);
888
- if (pageFeatures.length === 0) {
889
- break;
890
- }
891
- features.push(...pageFeatures);
892
- if (pageFeatures.length < limit) {
893
- break;
894
- }
852
+ for await (const page of drainOgcItemPages((pageRequest) => this.items(withOgcItemsPage(request, pageRequest)), {
853
+ pageSize: normalizePageSize(request.pageSize, request.limit),
854
+ maxPages: normalizeMaxPages(request.maxPages),
855
+ startOffset: normalizeOffset(request.offset),
856
+ ...(totalLimit !== undefined ? { totalLimit } : {}),
857
+ })) {
858
+ features.push(...page);
895
859
  }
896
860
  if (totalLimit !== undefined && features.length > totalLimit) {
897
861
  return features.slice(0, totalLimit);
@@ -899,24 +863,13 @@ export class HonuaOgcFeatureCollection {
899
863
  return features;
900
864
  }
901
865
  async *itemsStream(request = {}) {
902
- const pageSize = normalizePageSize(request.pageSize, request.limit);
903
- const maxPages = normalizeMaxPages(request.maxPages);
904
- const offset = normalizeOffset(request.offset);
905
- for (let page = 0; page < maxPages; page += 1) {
906
- const response = await this.items({
907
- ...request,
908
- limit: pageSize,
909
- offset: offset + page * pageSize,
910
- });
911
- const pageFeatures = extractOgcFeatures(response);
912
- if (pageFeatures.length === 0) {
913
- break;
914
- }
915
- yield pageFeatures;
916
- if (pageFeatures.length < pageSize) {
917
- break;
918
- }
919
- }
866
+ // The stream yields every page the drain produces; `limit` selects the
867
+ // page size here (as it always has) rather than capping the total.
868
+ yield* drainOgcItemPages((pageRequest) => this.items(withOgcItemsPage(request, pageRequest)), {
869
+ pageSize: normalizePageSize(request.pageSize, request.limit),
870
+ maxPages: normalizeMaxPages(request.maxPages),
871
+ startOffset: normalizeOffset(request.offset),
872
+ });
920
873
  }
921
874
  async item(request) {
922
875
  const layout = await this.layout();
@@ -1974,6 +1927,258 @@ function normalizeTotalLimit(limit) {
1974
1927
  }
1975
1928
  return limit;
1976
1929
  }
1930
+ /**
1931
+ * Query-string keys on an OGC API Features `rel="next"` link that must not be
1932
+ * replayed on the follow-up request: the drain owns the page size (`limit`)
1933
+ * and the caller owns the response format (`f`). Every other param on the
1934
+ * link — `offset` / `startindex` on an offset server, an opaque
1935
+ * `token` / `cursor` / `next` on a cursor server — is the server's own paging
1936
+ * state and is echoed back verbatim, subject to the stale-offset rules in
1937
+ * {@link ogcCursorFromHref}.
1938
+ */
1939
+ const OGC_NEXT_LINK_RESERVED_PARAMS = new Set(["f", "limit"]);
1940
+ /**
1941
+ * Params that express a numeric start position. Compared case-insensitively:
1942
+ * `offset` is Honua Server's spelling, `startindex` / `startIndex` the OGC API
1943
+ * spelling servers vary the casing of.
1944
+ */
1945
+ const OGC_OFFSET_PARAM_NAMES = new Set(["offset", "startindex"]);
1946
+ /**
1947
+ * Params that express an opaque paging cursor. Matched case-insensitively with
1948
+ * `-` / `_` stripped, so `page_token` and `pageToken` both hit. When one of
1949
+ * these drives the next link, the token owns the read position and any
1950
+ * `offset` / `startindex` on the same link is a stale echo of the request the
1951
+ * server just answered.
1952
+ */
1953
+ const OGC_CURSOR_PARAM_NAMES = new Set([
1954
+ "token",
1955
+ "nexttoken",
1956
+ "pagetoken",
1957
+ "continuationtoken",
1958
+ "resumptiontoken",
1959
+ "cursor",
1960
+ "next",
1961
+ "page",
1962
+ "searchafter",
1963
+ "scroll",
1964
+ "scrollid",
1965
+ ]);
1966
+ /** Base used only to parse relative `rel="next"` hrefs; never requested. */
1967
+ const OGC_NEXT_LINK_PARSE_BASE = "https://placeholder.test";
1968
+ /**
1969
+ * Page `/items` the way OGC API Features Core specifies: follow the
1970
+ * `rel="next"` link the server returns, and only fall back to `limit`/`offset`
1971
+ * arithmetic when the response advertises no usable next link. Offset
1972
+ * arithmetic alone silently mis-pages every server that pages by opaque token
1973
+ * or cursor (pgstac-backed APIs, several pygeoapi deployments): the cursor is
1974
+ * dropped, so pages repeat or skip with no error.
1975
+ *
1976
+ * Anti-corruption invariants, preserved on both paths:
1977
+ * - an empty page ends the drain;
1978
+ * - a page whose feature identity repeats an already-yielded page is dropped
1979
+ * and ends the drain, so a looping server can never duplicate features;
1980
+ * - a `rel="next"` link that repeats a cursor already followed is treated as
1981
+ * a stalled pager and ends the drain;
1982
+ * - a stale `offset` / `startindex` echoed on a cursor link never reaches the
1983
+ * wire beside the cursor (see {@link ogcCursorFromHref});
1984
+ * - `maxPages` still caps the total number of requests.
1985
+ *
1986
+ * On the link-driven path a short page is deliberately *not* a stop signal:
1987
+ * OGC API Features permits a server to return fewer than `limit` items on a
1988
+ * non-final page while still advertising `rel="next"` (the same tolerance the
1989
+ * Records drain applies). On the offset path a short page still ends the
1990
+ * drain, as it always has.
1991
+ */
1992
+ async function* drainOgcItemPages(fetchPage, options) {
1993
+ const { pageSize, maxPages, startOffset, totalLimit } = options;
1994
+ const seenCursorKeys = new Set();
1995
+ const seenPageKeys = new Set();
1996
+ let cursor;
1997
+ let emitted = 0;
1998
+ for (let page = 0; page < maxPages; page += 1) {
1999
+ const remaining = totalLimit === undefined ? pageSize : Math.max(0, totalLimit - emitted);
2000
+ if (remaining < 1) {
2001
+ break;
2002
+ }
2003
+ const limit = Math.min(pageSize, remaining);
2004
+ // A server cursor carries its own position; re-sending the computed offset
2005
+ // alongside it is what makes token pagers skip rows.
2006
+ const sentOffset = cursor === undefined ? startOffset + page * pageSize : undefined;
2007
+ const response = await fetchPage({
2008
+ limit,
2009
+ offset: sentOffset,
2010
+ cursorParams: cursor?.params,
2011
+ });
2012
+ const pageFeatures = extractOgcFeatures(response);
2013
+ if (pageFeatures.length === 0) {
2014
+ break;
2015
+ }
2016
+ const pageKey = ogcPageIdentityKey(pageFeatures);
2017
+ if (pageKey !== undefined) {
2018
+ if (seenPageKeys.has(pageKey)) {
2019
+ break;
2020
+ }
2021
+ seenPageKeys.add(pageKey);
2022
+ }
2023
+ yield pageFeatures;
2024
+ emitted += pageFeatures.length;
2025
+ const next = ogcNextItemsCursor(response.links, sentOffset);
2026
+ if (next !== undefined) {
2027
+ if (seenCursorKeys.has(next.key)) {
2028
+ break;
2029
+ }
2030
+ seenCursorKeys.add(next.key);
2031
+ cursor = next;
2032
+ continue;
2033
+ }
2034
+ if (cursor !== undefined) {
2035
+ // Link-driven drain and the server stopped advertising a next link:
2036
+ // that is the spec's end-of-results signal.
2037
+ break;
2038
+ }
2039
+ if (pageFeatures.length < limit) {
2040
+ break;
2041
+ }
2042
+ }
2043
+ }
2044
+ /**
2045
+ * Merge one {@link drainOgcItemPages} page request onto the caller's request.
2046
+ * Cursor params ride on `extraParams`, which the OGC wire layer serializes
2047
+ * before the typed fields, so the caller's `filter` / `bbox` / `crs` still win
2048
+ * and only the paging state comes from the server.
2049
+ */
2050
+ function withOgcItemsPage(request, page) {
2051
+ return {
2052
+ ...request,
2053
+ limit: page.limit,
2054
+ offset: page.offset,
2055
+ ...(page.cursorParams ? { extraParams: { ...request.extraParams, ...page.cursorParams } } : {}),
2056
+ };
2057
+ }
2058
+ /**
2059
+ * Resolve the server's next-page cursor from an `/items` response. Only the
2060
+ * link's query params are replayed (not its host or path), so a next link
2061
+ * rewritten by a proxy — or emitted with the server's internal hostname —
2062
+ * still pages correctly. Returns `undefined` when no `rel="next"` link carries
2063
+ * usable paging state, which is what selects the offset fallback.
2064
+ *
2065
+ * @param sentOffset the `offset` sent for the page that produced these links,
2066
+ * or `undefined` when a cursor already drove that request. Used to spot an
2067
+ * `offset` on the link that merely echoes the request just answered.
2068
+ */
2069
+ function ogcNextItemsCursor(links, sentOffset) {
2070
+ if (!links) {
2071
+ return undefined;
2072
+ }
2073
+ for (const link of links) {
2074
+ if (link.rel !== "next" || typeof link.href !== "string") {
2075
+ continue;
2076
+ }
2077
+ // An HTML alternate of the next page carries the same cursor but is not
2078
+ // the representation we are draining; prefer a data link when both exist.
2079
+ if (typeof link.type === "string" && link.type.includes("html")) {
2080
+ continue;
2081
+ }
2082
+ const cursor = ogcCursorFromHref(link.href, sentOffset);
2083
+ if (cursor !== undefined) {
2084
+ return cursor;
2085
+ }
2086
+ }
2087
+ return undefined;
2088
+ }
2089
+ /** Fold a query-param name to its comparison form (case / `-` / `_` insensitive). */
2090
+ function ogcParamKey(name) {
2091
+ let folded = "";
2092
+ for (const char of name) {
2093
+ if (char === "-" || char === "_")
2094
+ continue;
2095
+ folded += char.toLowerCase();
2096
+ }
2097
+ return folded;
2098
+ }
2099
+ /**
2100
+ * Lift the paging state off one `rel="next"` href.
2101
+ *
2102
+ * Servers routinely build the next link by preserving the query string of the
2103
+ * request they just answered and appending their cursor, so the link can carry
2104
+ * both a stale `offset=0` and a live `token=…`. Replaying both is exactly the
2105
+ * mis-paging this drain exists to prevent — a server that honors `offset`
2106
+ * alongside its own token re-reads or skips a page. Two rules keep the two
2107
+ * paging styles apart:
2108
+ *
2109
+ * 1. an opaque cursor on the link (`token`, `cursor`, `next`, `page`, …) owns
2110
+ * the read position, so every `offset` / `startindex` on that link is
2111
+ * dropped;
2112
+ * 2. otherwise an `offset` / `startindex` that merely repeats the offset just
2113
+ * sent cannot advance the drain, so it is dropped too — and if that leaves
2114
+ * the link with nothing, the drain falls back to offset arithmetic.
2115
+ *
2116
+ * A genuine offset link (`offset=10` after requesting `offset=0`) is kept and
2117
+ * drives the next page, which is what pygeoapi / ldproxy deployments emit.
2118
+ */
2119
+ function ogcCursorFromHref(href, sentOffset) {
2120
+ let url;
2121
+ try {
2122
+ url = new URL(href, OGC_NEXT_LINK_PARSE_BASE);
2123
+ }
2124
+ catch {
2125
+ return undefined;
2126
+ }
2127
+ const candidates = [];
2128
+ let hasOpaqueCursor = false;
2129
+ for (const [key, value] of url.searchParams) {
2130
+ if (OGC_NEXT_LINK_RESERVED_PARAMS.has(key)) {
2131
+ continue;
2132
+ }
2133
+ if (OGC_CURSOR_PARAM_NAMES.has(ogcParamKey(key))) {
2134
+ hasOpaqueCursor = true;
2135
+ }
2136
+ candidates.push([key, value]);
2137
+ }
2138
+ const params = {};
2139
+ let hasPagingState = false;
2140
+ for (const [key, value] of candidates) {
2141
+ const folded = ogcParamKey(key);
2142
+ if (OGC_OFFSET_PARAM_NAMES.has(folded)) {
2143
+ if (hasOpaqueCursor) {
2144
+ continue;
2145
+ }
2146
+ if (sentOffset !== undefined && Number(value) === sentOffset) {
2147
+ continue;
2148
+ }
2149
+ hasPagingState = true;
2150
+ }
2151
+ else if (OGC_CURSOR_PARAM_NAMES.has(folded)) {
2152
+ hasPagingState = true;
2153
+ }
2154
+ params[key] = value;
2155
+ }
2156
+ // Everything that survived is an echo of the query we just sent (`bbox`,
2157
+ // `filter`, `sortby`, …): the link advertises no forward progress, so the
2158
+ // offset arithmetic below stays in charge rather than re-reading this page.
2159
+ if (!hasPagingState) {
2160
+ return undefined;
2161
+ }
2162
+ const entries = Object.entries(params).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
2163
+ return { key: entries.map(([key, value]) => `${key}=${value}`).join("&"), params };
2164
+ }
2165
+ /**
2166
+ * Cheap identity signature for one `/items` page, used to detect a server that
2167
+ * replays a page it already returned. Returns `undefined` when any feature
2168
+ * lacks a GeoJSON `id`, in which case the drain simply skips the check rather
2169
+ * than serializing whole geometries.
2170
+ */
2171
+ function ogcPageIdentityKey(features) {
2172
+ const ids = [];
2173
+ for (const feature of features) {
2174
+ const id = feature?.id;
2175
+ if (typeof id !== "string" && typeof id !== "number") {
2176
+ return undefined;
2177
+ }
2178
+ ids.push(String(id));
2179
+ }
2180
+ return JSON.stringify(ids);
2181
+ }
1977
2182
  function isFinitePositiveInteger(value) {
1978
2183
  return typeof value === "number" && Number.isFinite(value) && Math.trunc(value) > 0;
1979
2184
  }
package/core/types.d.ts CHANGED
@@ -540,9 +540,33 @@ export interface HonuaClientOptions {
540
540
  transport?: HonuaTransport;
541
541
  }
542
542
  export interface HonuaRetryOptions {
543
+ /**
544
+ * Maximum number of transient-failure retry attempts after the initial
545
+ * request. Retry is entirely opt-in: values below `1` (including the default
546
+ * `0`) disable the retry loop, so no status, network, or timeout failure is
547
+ * replayed.
548
+ *
549
+ * This option does not govern the authentication replay. When an
550
+ * {@link HonuaClientOptions.auth} provider is configured, a replay-safe
551
+ * request (`GET` / `HEAD` / `PUT` / `DELETE`) answered with `401` or `403` is
552
+ * still reissued once after a forced credential refresh, even with retries
553
+ * disabled. Instrumentation that counts requests should expect that one extra
554
+ * attempt.
555
+ */
543
556
  maxRetries?: number;
557
+ /** First backoff delay in milliseconds; doubles per attempt. Default: `100`. */
544
558
  baseDelayMs?: number;
559
+ /** Upper bound on any single backoff delay, including a server `Retry-After`. Default: `2000`. */
545
560
  maxDelayMs?: number;
561
+ /**
562
+ * HTTP statuses the loop replays on replay-safe methods
563
+ * (`GET` / `HEAD` / `PUT` / `DELETE`). Default: `[429, 502, 503, 504]` — the
564
+ * statuses that explicitly invite the same request again. This is
565
+ * intentionally narrower than the broader `408, 429, 500, 502, 503, 504` set
566
+ * that {@link HonuaHttpError} classifies as transient `retryable` metadata:
567
+ * that classification is descriptive and never initiates a retry. Pass the
568
+ * broader set here to opt into replaying `408` / `500`.
569
+ */
546
570
  retryStatuses?: readonly number[];
547
571
  }
548
572
  export interface HonuaCompatibilityRequest {
@@ -1118,6 +1142,17 @@ export interface HonuaOgcLink {
1118
1142
  rel?: string;
1119
1143
  type?: string;
1120
1144
  title?: string;
1145
+ /**
1146
+ * HTTP method the link target expects. STAC API uses it on the landing
1147
+ * page (`rel="search"` advertised for `GET` and/or `POST`) and on
1148
+ * pagination links (a `POST` `rel="next"` carries its cursor in `body`
1149
+ * rather than on the href query string).
1150
+ */
1151
+ method?: string;
1152
+ /** Request-body members a `POST` link contributes to the next request. */
1153
+ body?: Record<string, unknown>;
1154
+ /** When true, `body` merges over the originating request body. */
1155
+ merge?: boolean;
1121
1156
  }
1122
1157
  /** Response from the OGC API landing page (`/ogc/features`). */
1123
1158
  export interface HonuaOgcLandingResponse {
@@ -1363,12 +1398,21 @@ export interface HonuaOgcProcessJobStatus {
1363
1398
  };
1364
1399
  }
1365
1400
  /**
1366
- * Process-execution request envelope. honua-server advertises
1367
- * `jobControlOptions: ['async-execute', 'dismiss']` and rejects any
1368
- * `response` other than `document` with HTTP 501 — the SDK type mirrors
1369
- * that supported surface (no `sync` mode, no `raw` response). `mode: "auto"`
1370
- * omits the `Prefer` header so the server applies its default; `"async"`
1371
- * sends `Prefer: respond-async` explicitly.
1401
+ * Process-execution request envelope for `POST /processes/{id}/execution`
1402
+ * (OGC API — Processes Part 1: Core §7.11).
1403
+ *
1404
+ * `mode` selects the RFC 7240 preference the SDK sends:
1405
+ *
1406
+ * - `"async"` → `Prefer: respond-async`. A conformant server answers `201` with
1407
+ * a `Location` header and a `statusInfo` body.
1408
+ * - `"sync"` → no `Prefer` header, and the caller asserts the process declares
1409
+ * `sync-execute`. A conformant server answers `200` with the results document.
1410
+ * - `"auto"` (default) → no `Prefer` header; whichever shape the server returns
1411
+ * is adapted onto the same `IJobRun` surface.
1412
+ *
1413
+ * Core defines no header that *forces* synchronous execution, so `"sync"` is a
1414
+ * preference plus a capability assertion, never a guarantee: a server that
1415
+ * answers a `"sync"` request with a job still yields a pollable `IJobRun`.
1372
1416
  */
1373
1417
  export interface OgcProcessExecuteRequest {
1374
1418
  processId: string;
@@ -1379,19 +1423,76 @@ export interface OgcProcessExecuteRequest {
1379
1423
  mediaType?: string;
1380
1424
  };
1381
1425
  }>;
1382
- /** `async-execute` returns a job; `auto` lets the server pick (always async on Honua). */
1383
- mode?: "async" | "auto";
1426
+ /** Execution preference; see the interface docs. Defaults to `"auto"`. */
1427
+ mode?: "sync" | "async" | "auto";
1428
+ /**
1429
+ * The process's advertised `jobControlOptions` (from `describe()` or a
1430
+ * discovery snapshot). Supplied — or already memoized from a `describe()` on
1431
+ * the same handle — it lets `mode` be gated against what the process actually
1432
+ * declares, so an undeclared `sync-execute` / `async-execute` fails closed with
1433
+ * `HonuaCapabilityNotSupportedError` instead of being posted blindly.
1434
+ */
1435
+ jobControlOptions?: readonly string[];
1384
1436
  headers?: HeadersInit;
1385
1437
  signal?: AbortSignal;
1438
+ /**
1439
+ * Raw endpoint path prefix (defaults to the Honua facade `/ogc/processes`). A
1440
+ * raw third-party Processes service root discovered by `discoverOgcProcesses()`
1441
+ * threads its service-root path here so the execution route resolves against
1442
+ * the advertised layout instead of the facade.
1443
+ */
1444
+ basePath?: string;
1445
+ }
1446
+ /**
1447
+ * Request envelope for the OGC API Processes job routes (status, results,
1448
+ * dismiss). Carries the same `basePath` seam as the metadata requests so a job
1449
+ * created against a raw service root keeps polling that root.
1450
+ */
1451
+ export interface OgcProcessJobRequest {
1452
+ jobId: string;
1453
+ signal?: AbortSignal;
1454
+ responseFormat?: string;
1455
+ extraParams?: Record<string, string | number | boolean>;
1456
+ /** Raw endpoint path prefix (defaults to the Honua facade `/ogc/processes`). */
1457
+ basePath?: string;
1458
+ /**
1459
+ * Server-advertised route for this job resource, already reduced to a
1460
+ * same-origin request path. When present it replaces the Core-templated
1461
+ * `{basePath}/jobs/{jobId}[/results]` route so the lifecycle follows the
1462
+ * server's own `Location` header and `links[]` rather than a guessed path.
1463
+ */
1464
+ routePath?: string;
1386
1465
  }
1387
- /** Job descriptor returned when the server accepted an async execution. */
1466
+ /**
1467
+ * Outcome of `POST /processes/{id}/execution`.
1468
+ *
1469
+ * The same envelope carries both Core response shapes so one typed surface
1470
+ * covers a raw third-party server and the Honua facade:
1471
+ *
1472
+ * - asynchronous — `jobID` names the created job and `synchronous` is `false`.
1473
+ * - synchronous — the server answered `200` with the results document inline.
1474
+ * `jobID` is `""` (Core assigns no job identifier to a synchronous
1475
+ * execution), `status` is `"successful"`, `synchronous` is `true`, and
1476
+ * `results` holds the document.
1477
+ */
1388
1478
  export interface HonuaOgcProcessJobAccepted {
1479
+ /** Server-assigned job id; `""` for a synchronous execution (no job exists). */
1389
1480
  jobID: string;
1390
1481
  status: OgcProcessStatus;
1391
1482
  processID?: string;
1392
1483
  links?: HonuaOgcLink[];
1393
1484
  /** Initial (often `accepted` or `running`) status payload, when reported. */
1394
1485
  statusInfo?: HonuaOgcProcessJobStatus;
1486
+ /**
1487
+ * Same-origin request path derived from the async `Location` header (or the
1488
+ * accepted document's `self` link). Polling follows it instead of templating
1489
+ * `{basePath}/jobs/{jobId}`.
1490
+ */
1491
+ statusPath?: string;
1492
+ /** Results document, populated only when `synchronous` is `true`. */
1493
+ results?: HonuaOgcProcessJobResults;
1494
+ /** `true` when the server answered inline with the results document. */
1495
+ synchronous?: boolean;
1395
1496
  }
1396
1497
  /**
1397
1498
  * Result document returned from `/jobs/{jobId}/results`. OGC API Processes
@@ -1422,6 +1523,19 @@ export interface StacSearchRequest {
1422
1523
  * advertise an opaque `?next=…` token instead of `offset`.
1423
1524
  */
1424
1525
  next?: string;
1526
+ /**
1527
+ * Opaque continuation query params captured verbatim from a server
1528
+ * `rel="next"` link whose cursor is neither `offset` nor `next` (pgstac /
1529
+ * stac-fastapi emit `?token=next:…`). Merged over the serialized search
1530
+ * params so the next page is requested exactly the way the server asked.
1531
+ */
1532
+ nextParams?: Readonly<Record<string, string>>;
1533
+ /**
1534
+ * Opaque continuation body members captured from a `POST` `rel="next"`
1535
+ * link (`{ "method": "POST", "body": { "token": "next:…" }, "merge": true }`).
1536
+ * Merged over the serialized `POST /search` body.
1537
+ */
1538
+ nextBody?: Readonly<Record<string, unknown>>;
1425
1539
  /** Subset of asset / item properties to return. */
1426
1540
  fields?: {
1427
1541
  include?: readonly string[];