@honua/app-platform 0.1.0-beta.0 → 0.1.2-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.
- package/contract/discovery.d.ts +231 -0
- package/contract/discovery.d.ts.map +1 -0
- package/contract/discovery.js +554 -0
- package/contract/edit-sketch.d.ts +8 -0
- package/contract/edit-sketch.d.ts.map +1 -1
- package/contract/edit-sketch.js +15 -0
- package/contract/edit-snapping.d.ts +160 -0
- package/contract/edit-snapping.d.ts.map +1 -0
- package/contract/edit-snapping.js +675 -0
- package/contract/generated/projjson-v0.7-crs-validator.d.ts +35 -0
- package/contract/generated/projjson-v0.7-crs-validator.d.ts.map +1 -0
- package/contract/generated/projjson-v0.7-crs-validator.js +3941 -0
- package/contract/index.d.ts +11 -2
- package/contract/index.d.ts.map +1 -1
- package/contract/index.js +4 -1
- package/contract/pmtiles.d.ts +31 -0
- package/contract/pmtiles.d.ts.map +1 -1
- package/contract/pmtiles.js +60 -20
- package/contract/protocol-module.d.ts +70 -0
- package/contract/protocol-module.d.ts.map +1 -0
- package/contract/protocol-module.js +1 -0
- package/contract/schema-envelope.d.ts +26 -0
- package/contract/schema-envelope.d.ts.map +1 -0
- package/contract/schema-envelope.js +25 -0
- package/contract/schema.d.ts +582 -0
- package/contract/schema.d.ts.map +1 -0
- package/contract/schema.js +2483 -0
- package/contract/source-capability-support.d.ts +6 -0
- package/contract/source-capability-support.d.ts.map +1 -0
- package/contract/source-capability-support.js +154 -0
- package/contract/source.d.ts +35 -17
- package/contract/source.d.ts.map +1 -1
- package/contract/source.js +267 -71
- package/contract/types.d.ts +140 -4
- package/contract/types.d.ts.map +1 -1
- package/contract/types.js +6 -14
- package/core/bounded-json.d.ts +4 -0
- package/core/bounded-json.d.ts.map +1 -0
- package/core/bounded-json.js +38 -0
- package/core/cache-state.d.ts +9 -0
- package/core/cache-state.d.ts.map +1 -1
- package/core/cache-state.js +10 -0
- package/core/capabilities-xml.d.ts +33 -0
- package/core/capabilities-xml.d.ts.map +1 -0
- package/core/capabilities-xml.js +331 -0
- package/core/client.d.ts +4 -1
- package/core/client.d.ts.map +1 -1
- package/core/client.js +115 -29
- package/core/error-classifications.d.ts +153 -0
- package/core/error-classifications.d.ts.map +1 -0
- package/core/error-classifications.js +140 -0
- package/core/error-code-registry.d.ts +158 -0
- package/core/error-code-registry.d.ts.map +1 -0
- package/core/error-code-registry.js +143 -0
- package/core/error-envelope.d.ts +68 -0
- package/core/error-envelope.d.ts.map +1 -0
- package/core/error-envelope.js +357 -0
- package/core/errors.d.ts +45 -29
- package/core/errors.d.ts.map +1 -1
- package/core/errors.js +97 -50
- package/core/geoparquet-sql.d.ts +30 -2
- package/core/geoparquet-sql.d.ts.map +1 -1
- package/core/geoparquet-sql.js +38 -3
- package/core/odata-key-path.d.ts +12 -0
- package/core/odata-key-path.d.ts.map +1 -0
- package/core/odata-key-path.js +28 -0
- package/core/odata-write-codec.d.ts +66 -0
- package/core/odata-write-codec.d.ts.map +1 -0
- package/core/odata-write-codec.js +1083 -0
- package/core/odata.d.ts +59 -7
- package/core/odata.d.ts.map +1 -1
- package/core/odata.js +431 -99
- package/core/ogc-features.d.ts.map +1 -1
- package/core/ogc-features.js +3 -0
- package/core/ogc-maps.d.ts +7 -0
- package/core/ogc-maps.d.ts.map +1 -1
- package/core/ogc-maps.js +46 -7
- package/core/ogc-processes.d.ts +2 -1
- package/core/ogc-processes.d.ts.map +1 -1
- package/core/ogc-processes.js +33 -5
- package/core/ogc-records.d.ts +9 -0
- package/core/ogc-records.d.ts.map +1 -1
- package/core/ogc-records.js +78 -29
- package/core/ogc-tiles.d.ts +10 -1
- package/core/ogc-tiles.d.ts.map +1 -1
- package/core/ogc-tiles.js +56 -17
- package/core/path-utils.d.ts +9 -0
- package/core/path-utils.d.ts.map +1 -1
- package/core/path-utils.js +19 -0
- package/core/protocol-transport.d.ts +1 -0
- package/core/protocol-transport.d.ts.map +1 -1
- package/core/request-pipeline.d.ts.map +1 -1
- package/core/request-pipeline.js +1 -1
- package/core/spatial-filter.d.ts +8 -4
- package/core/spatial-filter.d.ts.map +1 -1
- package/core/spatial-filter.js +206 -16
- package/core/stac-static.d.ts +6 -2
- package/core/stac-static.d.ts.map +1 -1
- package/core/stac-static.js +260 -69
- package/core/surfaces.d.ts.map +1 -1
- package/core/surfaces.js +69 -5
- package/core/types.d.ts +125 -7
- package/core/types.d.ts.map +1 -1
- package/core/wfs-capabilities.d.ts +2 -0
- package/core/wfs-capabilities.d.ts.map +1 -1
- package/core/wfs-capabilities.js +8 -1
- package/core/wfs.d.ts.map +1 -1
- package/core/wfs.js +10 -0
- package/core/wms-capabilities.d.ts +56 -84
- package/core/wms-capabilities.d.ts.map +1 -1
- package/core/wms-capabilities.js +212 -509
- package/core/wmts-capabilities.d.ts +62 -62
- package/core/wmts-capabilities.d.ts.map +1 -1
- package/core/wmts-capabilities.js +224 -371
- package/esri-compat/editor.d.ts +8 -0
- package/esri-compat/editor.d.ts.map +1 -1
- package/esri-compat/editor.js +13 -0
- package/esri-compat/feature-layer.d.ts +2 -0
- package/esri-compat/feature-layer.d.ts.map +1 -1
- package/esri-compat/feature-layer.js +1 -0
- package/esri-compat/layer-list.d.ts +11 -0
- package/esri-compat/layer-list.d.ts.map +1 -1
- package/esri-compat/layer-list.js +49 -0
- package/esri-compat/legend.d.ts +10 -0
- package/esri-compat/legend.d.ts.map +1 -1
- package/esri-compat/legend.js +27 -0
- package/esri-compat/portal.d.ts +197 -0
- package/esri-compat/portal.d.ts.map +1 -0
- package/esri-compat/portal.js +251 -0
- package/esri-compat/renderer-objects.d.ts +40 -0
- package/esri-compat/renderer-objects.d.ts.map +1 -0
- package/esri-compat/renderer-objects.js +62 -0
- package/esri-compat/sketch.d.ts +43 -0
- package/esri-compat/sketch.d.ts.map +1 -1
- package/esri-compat/sketch.js +46 -0
- package/esri-compat/snapping.d.ts +43 -0
- package/esri-compat/snapping.d.ts.map +1 -0
- package/esri-compat/snapping.js +45 -0
- package/esri-compat/url.d.ts.map +1 -1
- package/esri-compat/url.js +1 -1
- package/esri-compat/widget-host.d.ts +84 -0
- package/esri-compat/widget-host.d.ts.map +1 -0
- package/esri-compat/widget-host.js +159 -0
- package/generated-app/errors.d.ts +18 -2
- package/generated-app/errors.d.ts.map +1 -1
- package/generated-app/errors.js +44 -11
- package/geocoding/index.d.ts +7 -0
- package/geocoding/index.d.ts.map +1 -1
- package/geocoding/index.js +5 -0
- package/geocoding/provider-http.d.ts +37 -0
- package/geocoding/provider-http.d.ts.map +1 -0
- package/geocoding/provider-http.js +91 -0
- package/geocoding/provider.d.ts +145 -0
- package/geocoding/provider.d.ts.map +1 -0
- package/geocoding/provider.js +41 -0
- package/geocoding/providers/honua.d.ts +28 -0
- package/geocoding/providers/honua.d.ts.map +1 -0
- package/geocoding/providers/honua.js +68 -0
- package/geocoding/providers/nominatim.d.ts +47 -0
- package/geocoding/providers/nominatim.d.ts.map +1 -0
- package/geocoding/providers/nominatim.js +105 -0
- package/geocoding/providers/pelias.d.ts +40 -0
- package/geocoding/providers/pelias.d.ts.map +1 -0
- package/geocoding/providers/pelias.js +115 -0
- package/geocoding/providers/photon.d.ts +39 -0
- package/geocoding/providers/photon.d.ts.map +1 -0
- package/geocoding/providers/photon.js +107 -0
- package/geometry/convert.d.ts +13 -0
- package/geometry/convert.d.ts.map +1 -0
- package/geometry/convert.js +12 -0
- package/geometry/index.d.ts +17 -0
- package/geometry/index.d.ts.map +1 -0
- package/geometry/index.js +15 -0
- package/geometry/internal.d.ts +25 -0
- package/geometry/internal.d.ts.map +1 -0
- package/geometry/internal.js +40 -0
- package/geometry/ops.d.ts +71 -0
- package/geometry/ops.d.ts.map +1 -0
- package/geometry/ops.js +147 -0
- package/geometry/project.d.ts +40 -0
- package/geometry/project.d.ts.map +1 -0
- package/geometry/project.js +88 -0
- package/geometry/types.d.ts +36 -0
- package/geometry/types.d.ts.map +1 -0
- package/geometry/types.js +12 -0
- package/map/automatic-mount-integration.d.ts +134 -0
- package/map/automatic-mount-integration.d.ts.map +1 -0
- package/map/automatic-mount-integration.js +305 -0
- package/map/automatic-source-strategy.d.ts +125 -0
- package/map/automatic-source-strategy.d.ts.map +1 -0
- package/map/automatic-source-strategy.js +494 -0
- package/map/data-to-map-bridge.d.ts +293 -0
- package/map/data-to-map-bridge.d.ts.map +1 -0
- package/map/data-to-map-bridge.js +817 -0
- package/map/geojson-projection.d.ts +33 -0
- package/map/geojson-projection.d.ts.map +1 -0
- package/map/geojson-projection.js +118 -0
- package/map/index.d.ts +14 -0
- package/map/index.d.ts.map +1 -1
- package/map/index.js +8 -0
- package/map/raster-source-spec.d.ts +30 -0
- package/map/raster-source-spec.d.ts.map +1 -0
- package/map/raster-source-spec.js +190 -0
- package/map/raster-source-strategy.d.ts +65 -0
- package/map/raster-source-strategy.d.ts.map +1 -0
- package/map/raster-source-strategy.js +232 -0
- package/map/source-to-maplibre.d.ts +91 -0
- package/map/source-to-maplibre.d.ts.map +1 -0
- package/map/source-to-maplibre.js +454 -0
- package/map/temporal-playback.d.ts +139 -0
- package/map/temporal-playback.d.ts.map +1 -0
- package/map/temporal-playback.js +247 -0
- package/package.json +28 -2
- package/query-planner/canonical.d.ts +7 -0
- package/query-planner/canonical.d.ts.map +1 -0
- package/query-planner/canonical.js +145 -0
- package/query-planner/cql2-json.d.ts +17 -0
- package/query-planner/cql2-json.d.ts.map +1 -0
- package/query-planner/cql2-json.js +646 -0
- package/query-planner/diagnostics.d.ts +35 -0
- package/query-planner/diagnostics.d.ts.map +1 -0
- package/query-planner/diagnostics.js +373 -0
- package/query-planner/duckdb.d.ts +79 -0
- package/query-planner/duckdb.d.ts.map +1 -0
- package/query-planner/duckdb.js +758 -0
- package/query-planner/execution-context.d.ts +5 -0
- package/query-planner/execution-context.d.ts.map +1 -0
- package/query-planner/execution-context.js +50 -0
- package/query-planner/executor.d.ts +5 -0
- package/query-planner/executor.d.ts.map +1 -0
- package/query-planner/executor.js +223 -0
- package/query-planner/geoservices.d.ts +91 -0
- package/query-planner/geoservices.d.ts.map +1 -0
- package/query-planner/geoservices.js +667 -0
- package/query-planner/grpc.d.ts +100 -0
- package/query-planner/grpc.d.ts.map +1 -0
- package/query-planner/grpc.js +736 -0
- package/query-planner/index.d.ts +44 -0
- package/query-planner/index.d.ts.map +1 -0
- package/query-planner/index.js +30 -0
- package/query-planner/ir.d.ts +20 -0
- package/query-planner/ir.d.ts.map +1 -0
- package/query-planner/ir.js +417 -0
- package/query-planner/local-aggregate.d.ts +5 -0
- package/query-planner/local-aggregate.d.ts.map +1 -0
- package/query-planner/local-aggregate.js +126 -0
- package/query-planner/odata.d.ts +48 -0
- package/query-planner/odata.d.ts.map +1 -0
- package/query-planner/odata.js +662 -0
- package/query-planner/ogc-compiler.d.ts +55 -0
- package/query-planner/ogc-compiler.d.ts.map +1 -0
- package/query-planner/ogc-compiler.js +480 -0
- package/query-planner/ogc-features.d.ts +53 -0
- package/query-planner/ogc-features.d.ts.map +1 -0
- package/query-planner/ogc-features.js +617 -0
- package/query-planner/planner.d.ts +17 -0
- package/query-planner/planner.d.ts.map +1 -0
- package/query-planner/planner.js +818 -0
- package/query-planner/resource.d.ts +94 -0
- package/query-planner/resource.d.ts.map +1 -0
- package/query-planner/resource.js +526 -0
- package/query-planner/semantic-canonical.d.ts +15 -0
- package/query-planner/semantic-canonical.d.ts.map +1 -0
- package/query-planner/semantic-canonical.js +62 -0
- package/query-planner/semantic-compat.d.ts +10 -0
- package/query-planner/semantic-compat.d.ts.map +1 -0
- package/query-planner/semantic-compat.js +35 -0
- package/query-planner/semantic-compiler.d.ts +64 -0
- package/query-planner/semantic-compiler.d.ts.map +1 -0
- package/query-planner/semantic-compiler.js +81 -0
- package/query-planner/semantic-literals.d.ts +30 -0
- package/query-planner/semantic-literals.d.ts.map +1 -0
- package/query-planner/semantic-literals.js +133 -0
- package/query-planner/semantic-types.d.ts +265 -0
- package/query-planner/semantic-types.d.ts.map +1 -0
- package/query-planner/semantic-types.js +1 -0
- package/query-planner/semantic.d.ts +54 -0
- package/query-planner/semantic.d.ts.map +1 -0
- package/query-planner/semantic.js +1421 -0
- package/query-planner/types.d.ts +560 -0
- package/query-planner/types.d.ts.map +1 -0
- package/query-planner/types.js +49 -0
- package/query-planner/wfs-semantic.d.ts +58 -0
- package/query-planner/wfs-semantic.d.ts.map +1 -0
- package/query-planner/wfs-semantic.js +547 -0
- package/query-planner/wfs.d.ts +6 -0
- package/query-planner/wfs.d.ts.map +1 -0
- package/query-planner/wfs.js +71 -0
- package/realtime/contract.d.ts +142 -0
- package/realtime/contract.d.ts.map +1 -0
- package/realtime/contract.js +154 -0
- package/realtime/honua-server.d.ts.map +1 -1
- package/realtime/honua-server.js +13 -8
- package/realtime/index.d.ts +19 -4
- package/realtime/index.d.ts.map +1 -1
- package/realtime/index.js +14 -4
- package/realtime/odata-delta.d.ts +114 -0
- package/realtime/odata-delta.d.ts.map +1 -0
- package/realtime/odata-delta.js +483 -0
- package/realtime/resumable-transport.d.ts +111 -0
- package/realtime/resumable-transport.d.ts.map +1 -0
- package/realtime/resumable-transport.js +406 -0
- package/realtime/resumable.d.ts +104 -0
- package/realtime/resumable.d.ts.map +1 -0
- package/realtime/resumable.js +632 -0
- package/realtime/sse.d.ts.map +1 -1
- package/realtime/sse.js +27 -23
- package/realtime/websocket.d.ts +60 -0
- package/realtime/websocket.d.ts.map +1 -0
- package/realtime/websocket.js +157 -0
- package/realtime/wire.d.ts +27 -0
- package/realtime/wire.d.ts.map +1 -0
- package/realtime/wire.js +57 -0
- package/replica-sync/errors.d.ts +2 -1
- package/replica-sync/errors.d.ts.map +1 -1
- package/replica-sync/errors.js +33 -2
- package/runtime/edit-snapping.d.ts +107 -0
- package/runtime/edit-snapping.d.ts.map +1 -0
- package/runtime/edit-snapping.js +190 -0
- package/runtime/errors.d.ts +3 -2
- package/runtime/errors.d.ts.map +1 -1
- package/runtime/errors.js +19 -2
- package/runtime/index.d.ts +6 -0
- package/runtime/index.d.ts.map +1 -1
- package/runtime/index.js +3 -0
- package/runtime/maplibre-renderer.d.ts +41 -0
- package/runtime/maplibre-renderer.d.ts.map +1 -0
- package/runtime/maplibre-renderer.js +322 -0
- package/runtime/query-tiles.d.ts +2 -1
- package/runtime/query-tiles.d.ts.map +1 -1
- package/runtime/query-tiles.js +6 -2
- package/runtime/source-bridge.d.ts +4 -4
- package/runtime/source-bridge.d.ts.map +1 -1
- package/runtime/source-bridge.js +9 -56
- package/runtime/style-interactions.d.ts +3 -2
- package/runtime/style-interactions.d.ts.map +1 -1
- package/runtime/style-interactions.js +11 -3
- package/runtime/terra-draw-sketch.d.ts +275 -0
- package/runtime/terra-draw-sketch.d.ts.map +1 -0
- package/runtime/terra-draw-sketch.js +404 -0
- package/scene-workspace/index.d.ts +4 -0
- package/scene-workspace/index.d.ts.map +1 -1
- package/scene-workspace/index.js +2 -0
- package/scene-workspace/source-to-cesium.d.ts +116 -0
- package/scene-workspace/source-to-cesium.d.ts.map +1 -0
- package/scene-workspace/source-to-cesium.js +791 -0
- package/scene-workspace/state-sync.d.ts +147 -0
- package/scene-workspace/state-sync.d.ts.map +1 -0
- package/scene-workspace/state-sync.js +1123 -0
- package/source-capability-registry.d.ts +1 -0
- package/source-capability-registry.js +1 -0
- package/source-capability-types.d.ts +186 -0
- package/source-capability-types.js +7 -0
- package/style/index.d.ts +2 -0
- package/style/index.d.ts.map +1 -1
- package/style/index.js +2 -0
- package/style/renderers.d.ts +326 -0
- package/style/renderers.d.ts.map +1 -0
- package/style/renderers.js +517 -0
- package/web-components/controller.d.ts +2 -0
- package/web-components/controller.d.ts.map +1 -1
- package/web-components/controller.js +49 -8
- package/web-components/elements.d.ts +95 -1
- package/web-components/elements.d.ts.map +1 -1
- package/web-components/elements.js +458 -19
- package/web-components/index.d.ts +8 -1
- package/web-components/index.d.ts.map +1 -1
- package/web-components/index.js +7 -0
- package/web-components/maplibre-renderer.d.ts.map +1 -1
- package/web-components/maplibre-renderer.js +87 -0
- package/web-components/measurement.d.ts +69 -0
- package/web-components/measurement.d.ts.map +1 -0
- package/web-components/measurement.js +523 -0
- package/web-components/types.d.ts +109 -0
- package/web-components/types.d.ts.map +1 -1
- package/webmap/convert-renderer.d.ts +24 -3
- package/webmap/convert-renderer.d.ts.map +1 -1
- package/webmap/convert-renderer.js +93 -112
- package/webmap/index.d.ts +1 -1
- package/webmap/index.d.ts.map +1 -1
- package/webmap/index.js +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"odata-delta.d.ts","sourceRoot":"","sources":["../../../src/realtime/odata-delta.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,KAAK,EAIV,wBAAwB,EAEzB,MAAM,YAAY,CAAC;AAGpB,sFAAsF;AACtF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC;AAED,6FAA6F;AAC7F,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,8GAA8G;AAC9G,MAAM,WAAW,uBAAuB;IACtC,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uGAAuG;IACvG,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,0BAA0B,CAAC,QAAQ,GAAG,OAAO;IAC5D;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,wFAAwF;IACxF,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,SAAS,CAAC;IAC3D,sHAAsH;IACtH,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,QAAQ,CAAC;IAC5D,gGAAgG;IAChG,QAAQ,CAAC,YAAY,CAAC,EAAE,sBAAsB,CAAC;IAC/C,iFAAiF;IACjF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChD,wDAAwD;IACxD,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAClC;;;;OAIG;IACH,QAAQ,CAAC,0BAA0B,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC;IACjF;;;;;;OAMG;IACH,QAAQ,CAAC,wBAAwB,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,OAAO,CAAC;IACzE,sFAAsF;IACtF,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,4EAA4E;IAC5E,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,6FAA6F;IAC7F,QAAQ,CAAC,yBAAyB,CAAC,EAAE,MAAM,CAAC;IAC5C,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,uBAAuB,KAAK,IAAI,CAAC;IAC/D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAMD;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,GAAG,OAAO,EAC1D,OAAO,EAAE,0BAA0B,CAAC,QAAQ,CAAC,GAC5C,wBAAwB,CAAC,QAAQ,CAAC,CAkYpC"}
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OData v4 delta-link pull adapter for `@honua/sdk-js/realtime` (issue #558).
|
|
3
|
+
*
|
|
4
|
+
* OData delta links (`@odata.deltaLink`) are a *pull* change feed, not a
|
|
5
|
+
* socket transport: a client requests a scoped delta-link URL and the
|
|
6
|
+
* server returns only what changed since that link was minted, plus a fresh
|
|
7
|
+
* delta link for the next request. This module maps that request/response
|
|
8
|
+
* paging protocol onto the transport-neutral `RealtimeFeatureTransport`
|
|
9
|
+
* surface (`types.ts`, #556) so it composes with `createRealtimeFeatureStore`
|
|
10
|
+
* / `createResumableRealtimeSubscription` exactly like the SSE and WebSocket
|
|
11
|
+
* adapters (`sse.ts`, `websocket.ts`, #557) — with one deliberate
|
|
12
|
+
* difference dictated by REQ-004: this transport never claims to be live.
|
|
13
|
+
* `capabilities.kind` is `"polling"`, it never emits a `"heartbeat"`, and
|
|
14
|
+
* `onPoll` reports an explicit cadence/freshness snapshot on every request
|
|
15
|
+
* cycle so a consumer can render "checked N seconds ago, next check in M"
|
|
16
|
+
* rather than a live badge that misrepresents a poll as a stream.
|
|
17
|
+
*
|
|
18
|
+
* Ownership split from the SSE/WebSocket adapters: those open exactly one
|
|
19
|
+
* connection per `subscribe()` and never reconnect on their own, deferring
|
|
20
|
+
* reconnect entirely to `resumable-transport.ts`. This transport has no
|
|
21
|
+
* connection to reconnect — every cycle is an independent request/response —
|
|
22
|
+
* so it owns its own bounded poll loop, including the on-expiry resnapshot
|
|
23
|
+
* recovery a socket transport cannot perform for itself: an expired or
|
|
24
|
+
* rejected delta link (REQ per the User Workflow, "Expired/invalid delta
|
|
25
|
+
* state requires an explicit resnapshot") is recovered by re-running a full
|
|
26
|
+
* snapshot cycle and emitting a fresh `type: "snapshot", replace: true`
|
|
27
|
+
* event, bounded by `maxConsecutiveResnapshots` so a server that keeps
|
|
28
|
+
* rejecting the token cannot loop forever.
|
|
29
|
+
*
|
|
30
|
+
* Security posture (REQ-002): every `@odata.nextLink` / `@odata.deltaLink`
|
|
31
|
+
* this transport follows, and any caller-supplied `resumeFrom.deltaToken`,
|
|
32
|
+
* must resolve to the exact same origin and collection path as the
|
|
33
|
+
* configured `url`. A link that does not is rejected rather than followed —
|
|
34
|
+
* a delta token is an opaque, potentially attacker-influenced value once it
|
|
35
|
+
* has round-tripped through a checkpoint store, and this transport never
|
|
36
|
+
* trusts it to redirect a request elsewhere.
|
|
37
|
+
*
|
|
38
|
+
* @module
|
|
39
|
+
*/
|
|
40
|
+
import { HonuaRealtimeResumeError } from "./resumable.js";
|
|
41
|
+
import { realtimeFailure } from "./wire.js";
|
|
42
|
+
const DEFAULT_MAX_PAGES_PER_CYCLE = 500;
|
|
43
|
+
const DEFAULT_MAX_SNAPSHOT_ROWS = 50_000;
|
|
44
|
+
const DEFAULT_MAX_CONSECUTIVE_RESNAPSHOTS = 3;
|
|
45
|
+
/**
|
|
46
|
+
* Build a {@link RealtimeFeatureTransport} that polls an OData v4 entity
|
|
47
|
+
* set's delta link. See the module doc for the full recovery/security model.
|
|
48
|
+
*/
|
|
49
|
+
export function createOdataDeltaTransport(options) {
|
|
50
|
+
if (typeof options.entityId !== "function") {
|
|
51
|
+
throw new TypeError("createOdataDeltaTransport requires options.entityId.");
|
|
52
|
+
}
|
|
53
|
+
const pollIntervalMs = positiveInteger(options.pollIntervalMs, "pollIntervalMs");
|
|
54
|
+
const maxPagesPerCycle = positiveInteger(options.maxPagesPerCycle ?? DEFAULT_MAX_PAGES_PER_CYCLE, "maxPagesPerCycle");
|
|
55
|
+
const maxSnapshotRows = positiveInteger(options.maxSnapshotRows ?? DEFAULT_MAX_SNAPSHOT_ROWS, "maxSnapshotRows");
|
|
56
|
+
const maxConsecutiveResnapshots = positiveInteger(options.maxConsecutiveResnapshots ?? DEFAULT_MAX_CONSECUTIVE_RESNAPSHOTS, "maxConsecutiveResnapshots");
|
|
57
|
+
const collectionUrl = parseAbsoluteUrl(options.url, `createOdataDeltaTransport requires an absolute options.url ("${options.url}").`);
|
|
58
|
+
return {
|
|
59
|
+
capabilities: {
|
|
60
|
+
kind: "polling",
|
|
61
|
+
resumeModes: ["delta-token"],
|
|
62
|
+
emitsHeartbeats: false,
|
|
63
|
+
emitsWatermarks: false,
|
|
64
|
+
},
|
|
65
|
+
subscribe(request, observer) {
|
|
66
|
+
if (request.signal?.aborted) {
|
|
67
|
+
observer.complete();
|
|
68
|
+
return { close: () => { } };
|
|
69
|
+
}
|
|
70
|
+
const resumeDeltaToken = request.deltaToken ?? request.resumeFrom?.deltaToken;
|
|
71
|
+
let initialDeltaLink;
|
|
72
|
+
if (resumeDeltaToken !== undefined) {
|
|
73
|
+
const resumeUrl = parseRelativeOdataUrl(resumeDeltaToken, collectionUrl, `Realtime resumeFrom.deltaToken "${resumeDeltaToken}" is not a valid URL.`);
|
|
74
|
+
assertSameOdataCollection(collectionUrl, resumeUrl);
|
|
75
|
+
initialDeltaLink = resumeUrl.toString();
|
|
76
|
+
}
|
|
77
|
+
const now = () => options.now?.() ?? Date.now();
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
let closed = false;
|
|
80
|
+
let sequence = request.resumeFrom?.sequence ?? 0;
|
|
81
|
+
let resnapshotStreak = 0;
|
|
82
|
+
let timer;
|
|
83
|
+
const abortListener = () => teardown("Subscription signal was aborted.", true);
|
|
84
|
+
request.signal?.addEventListener("abort", abortListener, { once: true });
|
|
85
|
+
function teardown(reason, complete) {
|
|
86
|
+
if (closed)
|
|
87
|
+
return;
|
|
88
|
+
closed = true;
|
|
89
|
+
if (timer !== undefined)
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
request.signal?.removeEventListener("abort", abortListener);
|
|
92
|
+
if (!controller.signal.aborted)
|
|
93
|
+
controller.abort(reason);
|
|
94
|
+
if (complete)
|
|
95
|
+
observer.complete();
|
|
96
|
+
}
|
|
97
|
+
function fail(error) {
|
|
98
|
+
if (closed)
|
|
99
|
+
return;
|
|
100
|
+
closed = true;
|
|
101
|
+
if (timer !== undefined)
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
request.signal?.removeEventListener("abort", abortListener);
|
|
104
|
+
if (!controller.signal.aborted)
|
|
105
|
+
controller.abort("OData delta transport failed.");
|
|
106
|
+
observer.error(error);
|
|
107
|
+
}
|
|
108
|
+
function emit(event) {
|
|
109
|
+
if (closed)
|
|
110
|
+
return false;
|
|
111
|
+
try {
|
|
112
|
+
observer.next(event);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
catch (cause) {
|
|
116
|
+
fail(realtimeFailure("consumer-failed", "Realtime event observer rejected delivery.", cause));
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function reportPoll(partial) {
|
|
121
|
+
if (!options.onPoll)
|
|
122
|
+
return;
|
|
123
|
+
try {
|
|
124
|
+
options.onPoll({
|
|
125
|
+
polledAt: now(),
|
|
126
|
+
intervalMs: pollIntervalMs,
|
|
127
|
+
sequence,
|
|
128
|
+
changed: partial.changed,
|
|
129
|
+
upsertCount: partial.upsertCount,
|
|
130
|
+
deleteCount: partial.deleteCount,
|
|
131
|
+
...(partial.scheduled ? { nextPollAt: now() + pollIntervalMs } : {}),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Telemetry is diagnostic only; a throwing onPoll callback must
|
|
136
|
+
// never affect delivery.
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function schedulePoll(link) {
|
|
140
|
+
if (closed)
|
|
141
|
+
return;
|
|
142
|
+
timer = setTimeout(() => {
|
|
143
|
+
timer = undefined;
|
|
144
|
+
void runDeltaCycle(link);
|
|
145
|
+
}, pollIntervalMs);
|
|
146
|
+
}
|
|
147
|
+
async function fetchOdataDocument(target, trackChanges) {
|
|
148
|
+
const fetchImpl = resolveFetch(options.fetchImpl);
|
|
149
|
+
const headers = { Accept: "application/json", ...(options.headers?.() ?? {}) };
|
|
150
|
+
if (trackChanges)
|
|
151
|
+
headers.Prefer = "odata.track-changes";
|
|
152
|
+
let response;
|
|
153
|
+
try {
|
|
154
|
+
response = await fetchImpl(target, { method: "GET", headers, signal: controller.signal });
|
|
155
|
+
}
|
|
156
|
+
catch (cause) {
|
|
157
|
+
if (controller.signal.aborted)
|
|
158
|
+
throw cause;
|
|
159
|
+
throw realtimeFailure("transport-gap", "OData delta request failed.", cause);
|
|
160
|
+
}
|
|
161
|
+
if (!response.ok) {
|
|
162
|
+
const body = await safeReadJson(response);
|
|
163
|
+
const isExpired = (options.isDeltaLinkExpiredResponse ?? defaultIsDeltaLinkExpiredResponse)(response.status, body);
|
|
164
|
+
if (isExpired) {
|
|
165
|
+
throw new HonuaRealtimeResumeError("cursor-expired", `OData delta link expired or was rejected by the server (HTTP ${String(response.status)}).`);
|
|
166
|
+
}
|
|
167
|
+
throw realtimeFailure(response.status >= 500 ? "transport-gap" : "invalid-event", `OData delta request failed with HTTP ${String(response.status)}${response.statusText ? ` ${response.statusText}` : ""}.`, body);
|
|
168
|
+
}
|
|
169
|
+
const body = await safeReadJson(response);
|
|
170
|
+
if (!isRecord(body) || !Array.isArray(body.value)) {
|
|
171
|
+
throw realtimeFailure("invalid-event", "OData delta response is missing a JSON `value` array.", body);
|
|
172
|
+
}
|
|
173
|
+
return body;
|
|
174
|
+
}
|
|
175
|
+
function parseEntry(raw) {
|
|
176
|
+
if (!isRecord(raw)) {
|
|
177
|
+
throw realtimeFailure("invalid-event", "OData delta entry must be a JSON object.", raw);
|
|
178
|
+
}
|
|
179
|
+
const removed = raw["@removed"] ?? raw["@odata.removed"];
|
|
180
|
+
if (removed !== undefined) {
|
|
181
|
+
return { kind: "delete", patch: { id: removedEntryId(raw) } };
|
|
182
|
+
}
|
|
183
|
+
const isRelationshipDelta = options.isRelationshipDeltaEntry ?? defaultIsRelationshipDeltaEntry;
|
|
184
|
+
if (isRelationshipDelta(raw)) {
|
|
185
|
+
throw realtimeFailure("invalid-event", "OData relationship (link) deltas are not supported by createOdataDeltaTransport; only entity upserts and removals are normalized (REQ-003).", raw);
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const id = options.entityId(raw);
|
|
189
|
+
const feature = (options.toFeature ?? defaultToFeature)(raw);
|
|
190
|
+
return { kind: "upsert", patch: { id, feature } };
|
|
191
|
+
}
|
|
192
|
+
catch (cause) {
|
|
193
|
+
throw realtimeFailure("invalid-event", "OData delta entry projection (entityId/toFeature) failed.", cause);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function removedEntryId(entry) {
|
|
197
|
+
const withoutAnnotations = Object.fromEntries(Object.entries(entry).filter(([key]) => !key.startsWith("@")));
|
|
198
|
+
if (Object.keys(withoutAnnotations).length > 0) {
|
|
199
|
+
try {
|
|
200
|
+
return options.entityId(withoutAnnotations);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Fall through to @id parsing below.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const idValue = entry["@id"] ?? entry["@odata.id"];
|
|
207
|
+
if (typeof idValue !== "string") {
|
|
208
|
+
throw realtimeFailure("invalid-event", "OData removed delta entry has neither key properties nor an @id/@odata.id to derive an id from.", entry);
|
|
209
|
+
}
|
|
210
|
+
const key = parseODataIdKey(idValue);
|
|
211
|
+
if (key === undefined) {
|
|
212
|
+
throw realtimeFailure("invalid-event", `OData removed delta entry's id "${idValue}" uses a composite or unparseable key; the removed entry must retain key properties instead.`, entry);
|
|
213
|
+
}
|
|
214
|
+
return key;
|
|
215
|
+
}
|
|
216
|
+
async function runCycle(startUrl, mode) {
|
|
217
|
+
let target = startUrl;
|
|
218
|
+
let pages = 0;
|
|
219
|
+
const upserts = [];
|
|
220
|
+
const deletes = [];
|
|
221
|
+
let deltaLink;
|
|
222
|
+
while (true) {
|
|
223
|
+
pages += 1;
|
|
224
|
+
if (pages > maxPagesPerCycle) {
|
|
225
|
+
throw realtimeFailure("delivery-failed", `OData delta paging exceeded the configured ${String(maxPagesPerCycle)}-page bound.`, undefined);
|
|
226
|
+
}
|
|
227
|
+
const envelope = await fetchOdataDocument(target, mode === "snapshot" && pages === 1);
|
|
228
|
+
for (const raw of envelope.value) {
|
|
229
|
+
const parsed = parseEntry(raw);
|
|
230
|
+
if (parsed.kind === "delete")
|
|
231
|
+
deletes.push(parsed.patch);
|
|
232
|
+
else
|
|
233
|
+
upserts.push(parsed.patch);
|
|
234
|
+
if (mode === "snapshot" && upserts.length > maxSnapshotRows) {
|
|
235
|
+
throw realtimeFailure("delivery-failed", `OData snapshot exceeded the configured ${String(maxSnapshotRows)}-row bound.`, undefined);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const pageDeltaLink = envelope["@odata.deltaLink"];
|
|
239
|
+
if (pageDeltaLink !== undefined) {
|
|
240
|
+
const resolved = parseRelativeOdataUrl(pageDeltaLink, target, `OData response @odata.deltaLink "${pageDeltaLink}" is not a valid URL.`);
|
|
241
|
+
assertSameOdataCollection(collectionUrl, resolved);
|
|
242
|
+
deltaLink = resolved.toString();
|
|
243
|
+
}
|
|
244
|
+
const nextLink = envelope["@odata.nextLink"];
|
|
245
|
+
if (nextLink === undefined)
|
|
246
|
+
break;
|
|
247
|
+
const resolvedNext = parseRelativeOdataUrl(nextLink, target, `OData response @odata.nextLink "${nextLink}" is not a valid URL.`);
|
|
248
|
+
assertSameOdataCollection(collectionUrl, resolvedNext);
|
|
249
|
+
target = resolvedNext.toString();
|
|
250
|
+
}
|
|
251
|
+
if (deltaLink === undefined) {
|
|
252
|
+
throw realtimeFailure("invalid-event", "OData response did not include a terminal @odata.deltaLink; the server may not support delta-link change tracking for this collection.", undefined);
|
|
253
|
+
}
|
|
254
|
+
return { upserts, deletes, deltaLink };
|
|
255
|
+
}
|
|
256
|
+
function buildInitialUrl() {
|
|
257
|
+
const url = new URL(collectionUrl.toString());
|
|
258
|
+
const query = options.initialQuery;
|
|
259
|
+
if (query?.filter)
|
|
260
|
+
url.searchParams.set("$filter", query.filter);
|
|
261
|
+
if (query?.select?.length)
|
|
262
|
+
url.searchParams.set("$select", query.select.join(","));
|
|
263
|
+
if (query?.orderBy?.length)
|
|
264
|
+
url.searchParams.set("$orderby", query.orderBy.join(","));
|
|
265
|
+
if (typeof query?.top === "number" && Number.isFinite(query.top)) {
|
|
266
|
+
url.searchParams.set("$top", String(query.top));
|
|
267
|
+
}
|
|
268
|
+
return url.toString();
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Shared recovery path for a `cursor-expired` failure from *either*
|
|
272
|
+
* cycle: a real server should only ever reject a `$deltatoken`, but a
|
|
273
|
+
* caller-supplied `isDeltaLinkExpiredResponse` could in principle
|
|
274
|
+
* classify any response that way, so both cycles route through the
|
|
275
|
+
* same bounded streak counter (REQ-005) rather than the snapshot
|
|
276
|
+
* cycle failing closed on its very first retry.
|
|
277
|
+
*/
|
|
278
|
+
async function recoverFromExpiredDeltaLink(cause) {
|
|
279
|
+
resnapshotStreak += 1;
|
|
280
|
+
if (resnapshotStreak > maxConsecutiveResnapshots) {
|
|
281
|
+
fail(realtimeFailure("delivery-failed", `OData delta link expired ${String(resnapshotStreak)} times consecutively; giving up after the configured ${String(maxConsecutiveResnapshots)}-resnapshot bound.`, cause));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
reportPoll({ changed: false, upsertCount: 0, deleteCount: 0, scheduled: false });
|
|
285
|
+
const delivered = emit({
|
|
286
|
+
type: "status",
|
|
287
|
+
status: "reconnecting",
|
|
288
|
+
reason: "cursor-expired",
|
|
289
|
+
receivedAt: now(),
|
|
290
|
+
});
|
|
291
|
+
if (!delivered)
|
|
292
|
+
return;
|
|
293
|
+
await runSnapshotCycle();
|
|
294
|
+
}
|
|
295
|
+
async function runSnapshotCycle() {
|
|
296
|
+
try {
|
|
297
|
+
const cycle = await runCycle(buildInitialUrl(), "snapshot");
|
|
298
|
+
if (closed)
|
|
299
|
+
return;
|
|
300
|
+
resnapshotStreak = 0;
|
|
301
|
+
sequence += 1;
|
|
302
|
+
const delivered = emit({
|
|
303
|
+
type: "snapshot",
|
|
304
|
+
sequence,
|
|
305
|
+
deltaToken: cycle.deltaLink,
|
|
306
|
+
receivedAt: now(),
|
|
307
|
+
replace: true,
|
|
308
|
+
features: cycle.upserts,
|
|
309
|
+
});
|
|
310
|
+
if (!delivered)
|
|
311
|
+
return;
|
|
312
|
+
reportPoll({
|
|
313
|
+
changed: true,
|
|
314
|
+
upsertCount: cycle.upserts.length,
|
|
315
|
+
deleteCount: 0,
|
|
316
|
+
scheduled: true,
|
|
317
|
+
});
|
|
318
|
+
schedulePoll(cycle.deltaLink);
|
|
319
|
+
}
|
|
320
|
+
catch (cause) {
|
|
321
|
+
if (closed)
|
|
322
|
+
return;
|
|
323
|
+
if (cause instanceof HonuaRealtimeResumeError && cause.code === "cursor-expired") {
|
|
324
|
+
await recoverFromExpiredDeltaLink(cause);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
fail(cause);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async function runDeltaCycle(link) {
|
|
331
|
+
try {
|
|
332
|
+
const cycle = await runCycle(link, "delta");
|
|
333
|
+
if (closed)
|
|
334
|
+
return;
|
|
335
|
+
resnapshotStreak = 0;
|
|
336
|
+
const changed = cycle.upserts.length > 0 || cycle.deletes.length > 0;
|
|
337
|
+
if (changed)
|
|
338
|
+
sequence += 1;
|
|
339
|
+
const delivered = emit(changed
|
|
340
|
+
? {
|
|
341
|
+
type: "delta",
|
|
342
|
+
sequence,
|
|
343
|
+
deltaToken: cycle.deltaLink,
|
|
344
|
+
receivedAt: now(),
|
|
345
|
+
...(cycle.upserts.length ? { upserts: cycle.upserts } : {}),
|
|
346
|
+
...(cycle.deletes.length ? { deletes: cycle.deletes } : {}),
|
|
347
|
+
}
|
|
348
|
+
: {
|
|
349
|
+
type: "status",
|
|
350
|
+
status: "live",
|
|
351
|
+
sequence,
|
|
352
|
+
deltaToken: cycle.deltaLink,
|
|
353
|
+
receivedAt: now(),
|
|
354
|
+
reason: "poll-unchanged",
|
|
355
|
+
});
|
|
356
|
+
if (!delivered)
|
|
357
|
+
return;
|
|
358
|
+
reportPoll({
|
|
359
|
+
changed,
|
|
360
|
+
upsertCount: cycle.upserts.length,
|
|
361
|
+
deleteCount: cycle.deletes.length,
|
|
362
|
+
scheduled: true,
|
|
363
|
+
});
|
|
364
|
+
schedulePoll(cycle.deltaLink);
|
|
365
|
+
}
|
|
366
|
+
catch (cause) {
|
|
367
|
+
if (closed)
|
|
368
|
+
return;
|
|
369
|
+
if (cause instanceof HonuaRealtimeResumeError && cause.code === "cursor-expired") {
|
|
370
|
+
await recoverFromExpiredDeltaLink(cause);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
fail(cause);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
void (initialDeltaLink !== undefined ? runDeltaCycle(initialDeltaLink) : runSnapshotCycle());
|
|
377
|
+
return {
|
|
378
|
+
close() {
|
|
379
|
+
teardown("Subscription closed by caller.", true);
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function defaultIsDeltaLinkExpiredResponse(status) {
|
|
386
|
+
return status === 410;
|
|
387
|
+
}
|
|
388
|
+
function defaultIsRelationshipDeltaEntry(entry) {
|
|
389
|
+
return typeof entry.source === "string" && typeof entry.relationship === "string" && typeof entry.target === "string";
|
|
390
|
+
}
|
|
391
|
+
function defaultToFeature(entity) {
|
|
392
|
+
const clean = {};
|
|
393
|
+
for (const [key, value] of Object.entries(entity)) {
|
|
394
|
+
if (!key.startsWith("@"))
|
|
395
|
+
clean[key] = value;
|
|
396
|
+
}
|
|
397
|
+
return clean;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Reject a next/delta link that does not resolve to the exact origin and
|
|
401
|
+
* collection path this transport was configured for (REQ-002). Trailing
|
|
402
|
+
* slashes are normalized so `.../Incidents` and `.../Incidents/` are treated
|
|
403
|
+
* as the same collection.
|
|
404
|
+
*/
|
|
405
|
+
function assertSameOdataCollection(expected, candidate) {
|
|
406
|
+
if (candidate.origin !== expected.origin || normalizedPathname(candidate) !== normalizedPathname(expected)) {
|
|
407
|
+
throw new HonuaRealtimeResumeError("invalid-event", `OData delta/next link "${candidate.toString()}" does not resolve to the configured collection "${expected.toString()}"; refusing to follow a foreign link.`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function normalizedPathname(url) {
|
|
411
|
+
return url.pathname.length > 1 && url.pathname.endsWith("/") ? url.pathname.slice(0, -1) : url.pathname;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Parse the single-value key predicate out of an OData `@id`/`@odata.id`
|
|
415
|
+
* value like `Incidents('AB-12')` or `Incidents(42)`. Returns `undefined`
|
|
416
|
+
* for composite keys (`Key1=1,Key2='a'`) or an unparseable shape — callers
|
|
417
|
+
* treat that as unsupported rather than guessing.
|
|
418
|
+
*/
|
|
419
|
+
function parseODataIdKey(idValue) {
|
|
420
|
+
const open = idValue.lastIndexOf("(");
|
|
421
|
+
if (open === -1 || !idValue.endsWith(")"))
|
|
422
|
+
return undefined;
|
|
423
|
+
const inner = idValue.slice(open + 1, -1);
|
|
424
|
+
if (inner.includes("=") || hasTopLevelComma(inner))
|
|
425
|
+
return undefined;
|
|
426
|
+
const unquoted = inner.startsWith("'") && inner.endsWith("'") ? inner.slice(1, -1).replace(/''/g, "'") : inner;
|
|
427
|
+
if (unquoted.length === 0)
|
|
428
|
+
return undefined;
|
|
429
|
+
if (/^-?\d+$/.test(unquoted) && Number.isSafeInteger(Number(unquoted)))
|
|
430
|
+
return Number(unquoted);
|
|
431
|
+
return unquoted;
|
|
432
|
+
}
|
|
433
|
+
function hasTopLevelComma(inner) {
|
|
434
|
+
let quoted = false;
|
|
435
|
+
for (const character of inner) {
|
|
436
|
+
if (character === "'")
|
|
437
|
+
quoted = !quoted;
|
|
438
|
+
else if (character === "," && !quoted)
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
function parseAbsoluteUrl(input, message) {
|
|
444
|
+
try {
|
|
445
|
+
return new URL(input);
|
|
446
|
+
}
|
|
447
|
+
catch (cause) {
|
|
448
|
+
throw new TypeError(message, { cause });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function parseRelativeOdataUrl(value, base, message) {
|
|
452
|
+
try {
|
|
453
|
+
return new URL(value, base);
|
|
454
|
+
}
|
|
455
|
+
catch (cause) {
|
|
456
|
+
throw new HonuaRealtimeResumeError("invalid-event", message, { cause });
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
function resolveFetch(custom) {
|
|
460
|
+
if (custom)
|
|
461
|
+
return custom;
|
|
462
|
+
const globalFetch = globalThis.fetch;
|
|
463
|
+
if (!globalFetch)
|
|
464
|
+
throw new Error("fetch is not available; provide fetchImpl for this runtime.");
|
|
465
|
+
return globalFetch.bind(globalThis);
|
|
466
|
+
}
|
|
467
|
+
async function safeReadJson(response) {
|
|
468
|
+
try {
|
|
469
|
+
return await response.json();
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function isRecord(value) {
|
|
476
|
+
return typeof value === "object" && value !== null;
|
|
477
|
+
}
|
|
478
|
+
function positiveInteger(value, name) {
|
|
479
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
480
|
+
throw new TypeError(`createOdataDeltaTransport: ${name} must be a safe integer greater than zero.`);
|
|
481
|
+
}
|
|
482
|
+
return value;
|
|
483
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded, resumable SSE and WebSocket transports for `@honua/sdk-js/realtime`
|
|
3
|
+
* (issue #557), built on the #556 snapshot/delta/cursor/resume/plan-identity
|
|
4
|
+
* contract.
|
|
5
|
+
*
|
|
6
|
+
* `resumable.ts` deliberately never reconnects a transport — that ownership
|
|
7
|
+
* is explicitly deferred to this module (see
|
|
8
|
+
* `docs/decisions/realtime-snapshot-delta-cursor-resume-plan-identity-contract.md`,
|
|
9
|
+
* "the gate never reconnects a transport itself"). This module closes that
|
|
10
|
+
* gap by wrapping any raw {@link RealtimeFeatureTransport} (the SSE adapter
|
|
11
|
+
* in `sse.ts`, the WebSocket adapter in `websocket.ts`, or a custom one) with:
|
|
12
|
+
*
|
|
13
|
+
* - the bounded delivery gate from `resumable.ts` (`maxPendingEvents`,
|
|
14
|
+
* duplicate/gap detection, cross-scope checkpoint rejection);
|
|
15
|
+
* - reconnect ownership with exponential-backoff-and-jitter retry, bounded by
|
|
16
|
+
* `reconnect.maxAttempts` — exhausting attempts fails closed rather than
|
|
17
|
+
* retrying forever;
|
|
18
|
+
* - a heartbeat/liveness timeout that treats prolonged silence as a
|
|
19
|
+
* transport-detected gap;
|
|
20
|
+
* - resume-vs-resnapshot selection driven entirely by the gate's own
|
|
21
|
+
* `phase`: a reconnect resumes from the last accepted checkpoint's cursor
|
|
22
|
+
* only while the gate considers it valid, and requests a fresh snapshot
|
|
23
|
+
* the moment the gate (or this module, via `requireResnapshot`) decides
|
|
24
|
+
* otherwise — REQ-003's "resume only with a valid scoped cursor";
|
|
25
|
+
* - idempotent disposal that stops timers, the active connection, and the
|
|
26
|
+
* delivery gate exactly once, however disposal was triggered
|
|
27
|
+
* (caller `close()`, external `AbortSignal`, or an unrecoverable failure);
|
|
28
|
+
* - redacted telemetry (`onTelemetry`) built on `contract.ts`'s
|
|
29
|
+
* `deriveRealtimeContractAuthority` / `redactRealtimeCheckpoint`, so a raw
|
|
30
|
+
* cursor, watermark, or delta-token never reaches a log or metrics sink.
|
|
31
|
+
*
|
|
32
|
+
* The result still satisfies `RealtimeFeatureTransport`, so it composes with
|
|
33
|
+
* `createRealtimeFeatureStore` exactly like any other transport.
|
|
34
|
+
*
|
|
35
|
+
* @module
|
|
36
|
+
*/
|
|
37
|
+
import type { RealtimeContractAuthority, RedactedRealtimeCheckpointV1 } from "./contract.js";
|
|
38
|
+
import { type RealtimeCheckpointStore, type RealtimeDurableCheckpointV1, type RealtimeResumeContextV1 } from "./resumable.js";
|
|
39
|
+
import type { RealtimeServerSentEventsTransportOptions } from "./sse.js";
|
|
40
|
+
import type { RealtimeFeatureTransport } from "./types.js";
|
|
41
|
+
import type { RealtimeWebSocketTransportOptions } from "./websocket.js";
|
|
42
|
+
export interface RealtimeReconnectPolicy {
|
|
43
|
+
/** Consecutive reconnect attempts allowed before failing closed. `0` disables reconnect entirely. @default 8 */
|
|
44
|
+
readonly maxAttempts?: number;
|
|
45
|
+
/** Delay before the first reconnect attempt. @default 250 */
|
|
46
|
+
readonly baseDelayMs?: number;
|
|
47
|
+
/** Upper bound on the exponential backoff delay, before jitter. @default 30000 */
|
|
48
|
+
readonly maxDelayMs?: number;
|
|
49
|
+
/** How long a connection must stay live before a subsequent failure resets the attempt counter. @default 15000 */
|
|
50
|
+
readonly resetAfterMs?: number;
|
|
51
|
+
}
|
|
52
|
+
export interface RealtimeResumableTransportTelemetry {
|
|
53
|
+
readonly authority: RealtimeContractAuthority;
|
|
54
|
+
readonly reconnectAttempt: number;
|
|
55
|
+
readonly acceptedEventCount: number;
|
|
56
|
+
readonly duplicateEventCount: number;
|
|
57
|
+
readonly gapCount: number;
|
|
58
|
+
readonly overflowCount: number;
|
|
59
|
+
/** Never a raw cursor, watermark, or delta-token value. */
|
|
60
|
+
readonly checkpoint?: RedactedRealtimeCheckpointV1;
|
|
61
|
+
}
|
|
62
|
+
export interface RealtimeResumableTransportOptions<TFeature = unknown> {
|
|
63
|
+
readonly context: RealtimeResumeContextV1;
|
|
64
|
+
readonly checkpointStore?: RealtimeCheckpointStore;
|
|
65
|
+
/**
|
|
66
|
+
* Seed resume position for the very first connection, validated the same
|
|
67
|
+
* way a loaded `checkpointStore` checkpoint is (`evaluateRealtimeCheckpoint`).
|
|
68
|
+
* This is the only supported way to seed a resume position: `subscribe()`'s
|
|
69
|
+
* `request.cursor` / `resumeFrom` and similar fields are ignored by this
|
|
70
|
+
* wrapper so a stale or unscoped caller-supplied position can never bypass
|
|
71
|
+
* checkpoint compatibility validation.
|
|
72
|
+
*/
|
|
73
|
+
readonly initialCheckpoint?: RealtimeDurableCheckpointV1;
|
|
74
|
+
/** Includes the currently applying event. @default 64 */
|
|
75
|
+
readonly maxPendingEvents?: number;
|
|
76
|
+
/** Bounded event-id history persisted with each checkpoint. @default 256 */
|
|
77
|
+
readonly maxRecentEventIds?: number;
|
|
78
|
+
readonly reconnect?: RealtimeReconnectPolicy;
|
|
79
|
+
/** No event (including heartbeats) within this window is treated as a transport-detected gap. Disabled when omitted. */
|
|
80
|
+
readonly heartbeatTimeoutMs?: number;
|
|
81
|
+
/** Forwarded to {@link deriveRealtimeContractAuthority} for telemetry. */
|
|
82
|
+
readonly staleAfterMs?: number;
|
|
83
|
+
readonly onTelemetry?: (telemetry: RealtimeResumableTransportTelemetry) => void;
|
|
84
|
+
readonly now?: () => number;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Exponential backoff with jitter in the [0.5x, 1x] band for the given
|
|
88
|
+
* zero-based reconnect attempt — the same shape as
|
|
89
|
+
* `core/request-pipeline.ts`'s `resolveRetryDelayMs`, reimplemented locally
|
|
90
|
+
* so `@honua/sdk-js/realtime` does not pull in the HTTP request pipeline's
|
|
91
|
+
* error/type graph for one small helper. Exported so the schedule can be
|
|
92
|
+
* asserted deterministically without driving a fake transport end-to-end.
|
|
93
|
+
*/
|
|
94
|
+
export declare function computeReconnectDelayMs(policy: RealtimeReconnectPolicy | undefined, attempt: number): number;
|
|
95
|
+
/**
|
|
96
|
+
* Wrap a raw {@link RealtimeFeatureTransport} with bounded, resumable
|
|
97
|
+
* delivery: the #556 delivery gate, reconnect/backoff, heartbeat timeout,
|
|
98
|
+
* and redacted telemetry. See the module doc for the full behavior.
|
|
99
|
+
*/
|
|
100
|
+
export declare function createResumableRealtimeTransport<TFeature = unknown>(transport: RealtimeFeatureTransport<TFeature>, options: RealtimeResumableTransportOptions<TFeature>): RealtimeFeatureTransport<TFeature>;
|
|
101
|
+
/**
|
|
102
|
+
* Convenience factory combining {@link createRealtimeServerSentEventsTransport}
|
|
103
|
+
* with {@link createResumableRealtimeTransport}.
|
|
104
|
+
*/
|
|
105
|
+
export declare function createResumableServerSentEventsTransport<TFeature = unknown>(sseOptions: RealtimeServerSentEventsTransportOptions<TFeature>, resumableOptions: RealtimeResumableTransportOptions<TFeature>): RealtimeFeatureTransport<TFeature>;
|
|
106
|
+
/**
|
|
107
|
+
* Convenience factory combining {@link createRealtimeWebSocketTransport}
|
|
108
|
+
* with {@link createResumableRealtimeTransport}.
|
|
109
|
+
*/
|
|
110
|
+
export declare function createResumableWebSocketTransport<TFeature = unknown>(webSocketOptions: RealtimeWebSocketTransportOptions<TFeature>, resumableOptions: RealtimeResumableTransportOptions<TFeature>): RealtimeFeatureTransport<TFeature>;
|
|
111
|
+
//# sourceMappingURL=resumable-transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resumable-transport.d.ts","sourceRoot":"","sources":["../../../src/realtime/resumable-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAGH,OAAO,KAAK,EAAE,yBAAyB,EAAE,4BAA4B,EAAE,MAAM,eAAe,CAAC;AAC7F,OAAO,EAEL,KAAK,uBAAuB,EAC5B,KAAK,2BAA2B,EAEhC,KAAK,uBAAuB,EAG7B,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAE,wCAAwC,EAAE,MAAM,UAAU,CAAC;AACzE,OAAO,KAAK,EAGV,wBAAwB,EAGzB,MAAM,YAAY,CAAC;AAEpB,OAAO,KAAK,EAAE,iCAAiC,EAAE,MAAM,gBAAgB,CAAC;AAExE,MAAM,WAAW,uBAAuB;IACtC,gHAAgH;IAChH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,6DAA6D;IAC7D,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,kFAAkF;IAClF,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,kHAAkH;IAClH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,mCAAmC;IAClD,QAAQ,CAAC,SAAS,EAAE,yBAAyB,CAAC;IAC9C,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,2DAA2D;IAC3D,QAAQ,CAAC,UAAU,CAAC,EAAE,4BAA4B,CAAC;CACpD;AAED,MAAM,WAAW,iCAAiC,CAAC,QAAQ,GAAG,OAAO;IACnE,QAAQ,CAAC,OAAO,EAAE,uBAAuB,CAAC;IAC1C,QAAQ,CAAC,eAAe,CAAC,EAAE,uBAAuB,CAAC;IACnD;;;;;;;OAOG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,2BAA2B,CAAC;IACzD,yDAAyD;IACzD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,4EAA4E;IAC5E,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,SAAS,CAAC,EAAE,uBAAuB,CAAC;IAC7C,wHAAwH;IACxH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,0EAA0E;IAC1E,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,mCAAmC,KAAK,IAAI,CAAC;IAChF,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAkCD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,uBAAuB,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAK5G;AAED;;;;GAIG;AACH,wBAAgB,gCAAgC,CAAC,QAAQ,GAAG,OAAO,EACjE,SAAS,EAAE,wBAAwB,CAAC,QAAQ,CAAC,EAC7C,OAAO,EAAE,iCAAiC,CAAC,QAAQ,CAAC,GACnD,wBAAwB,CAAC,QAAQ,CAAC,CA+TpC;AAED;;;GAGG;AACH,wBAAgB,wCAAwC,CAAC,QAAQ,GAAG,OAAO,EACzE,UAAU,EAAE,wCAAwC,CAAC,QAAQ,CAAC,EAC9D,gBAAgB,EAAE,iCAAiC,CAAC,QAAQ,CAAC,GAC5D,wBAAwB,CAAC,QAAQ,CAAC,CAKpC;AAED;;;GAGG;AACH,wBAAgB,iCAAiC,CAAC,QAAQ,GAAG,OAAO,EAClE,gBAAgB,EAAE,iCAAiC,CAAC,QAAQ,CAAC,EAC7D,gBAAgB,EAAE,iCAAiC,CAAC,QAAQ,CAAC,GAC5D,wBAAwB,CAAC,QAAQ,CAAC,CAKpC"}
|