@honua/app-platform 0.1.0-beta.0 → 0.1.1-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,1123 @@
|
|
|
1
|
+
export const SCENE_STATE_SYNC_KIND = "honua.scene-state-sync";
|
|
2
|
+
export const SCENE_STATE_SYNC_VERSION = "1.0";
|
|
3
|
+
export const SCENE_STATE_SYNC_SLICES = [
|
|
4
|
+
"camera",
|
|
5
|
+
"selection",
|
|
6
|
+
"filters",
|
|
7
|
+
"time",
|
|
8
|
+
"detail",
|
|
9
|
+
"attribution",
|
|
10
|
+
"realtime",
|
|
11
|
+
];
|
|
12
|
+
export class HonuaSceneStateSyncError extends Error {
|
|
13
|
+
code;
|
|
14
|
+
constructor(code, message, options = {}) {
|
|
15
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.name = "HonuaSceneStateSyncError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const DEFAULT_MAX_PORTS = 8;
|
|
21
|
+
const DEFAULT_MAX_DIAGNOSTICS = 128;
|
|
22
|
+
const HARD_MAX_PORTS = 64;
|
|
23
|
+
const HARD_MAX_DIAGNOSTICS = 2_048;
|
|
24
|
+
const MAX_SYNCHRONOUS_SUBSCRIBE_EVENTS = 32;
|
|
25
|
+
const MAX_SELECTION = 2_048;
|
|
26
|
+
const MAX_FILTERS = 128;
|
|
27
|
+
const MAX_ATTRIBUTIONS = 128;
|
|
28
|
+
const MAX_JSON_NODES = 4_096;
|
|
29
|
+
const MAX_JSON_DEPTH = 24;
|
|
30
|
+
const MAX_JSON_PROPERTIES = 256;
|
|
31
|
+
const MAX_FOREIGN_ARRAY = 2_048;
|
|
32
|
+
const MAX_FOREIGN_NODES = 16_384;
|
|
33
|
+
const MAX_FOREIGN_STRING = 1_048_576;
|
|
34
|
+
const MAX_FOREIGN_STRING_TOTAL = 4_194_304;
|
|
35
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
36
|
+
/** Create one isolated renderer-neutral state synchronization lifecycle. */
|
|
37
|
+
export function createSceneStateSynchronizer(options) {
|
|
38
|
+
const normalized = normalizeOptions(options);
|
|
39
|
+
const attachments = new Map();
|
|
40
|
+
const listeners = new Set();
|
|
41
|
+
const values = {};
|
|
42
|
+
const diagnostics = [];
|
|
43
|
+
const pending = new Map();
|
|
44
|
+
let revision = 0;
|
|
45
|
+
let generation = 0;
|
|
46
|
+
let timer;
|
|
47
|
+
let microtaskScheduled = false;
|
|
48
|
+
let disposed = false;
|
|
49
|
+
let removeAbortListener;
|
|
50
|
+
function now() {
|
|
51
|
+
return canonicalTimestamp(normalized.now(), "clock");
|
|
52
|
+
}
|
|
53
|
+
function ensureLive() {
|
|
54
|
+
if (disposed)
|
|
55
|
+
throw new HonuaSceneStateSyncError("disposed", "Scene state synchronizer is disposed");
|
|
56
|
+
}
|
|
57
|
+
function snapshot() {
|
|
58
|
+
return Object.freeze({
|
|
59
|
+
kind: SCENE_STATE_SYNC_KIND,
|
|
60
|
+
version: SCENE_STATE_SYNC_VERSION,
|
|
61
|
+
applicationId: normalized.applicationId,
|
|
62
|
+
revision,
|
|
63
|
+
values: Object.freeze({ ...values }),
|
|
64
|
+
diagnostics: Object.freeze([...diagnostics]),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function emit(event) {
|
|
68
|
+
for (const listener of [...listeners]) {
|
|
69
|
+
try {
|
|
70
|
+
listener(event);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Observers cannot corrupt state or renderer ownership.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function diagnose(code, portId, message, detail = {}) {
|
|
78
|
+
let diagnosticAt;
|
|
79
|
+
try {
|
|
80
|
+
diagnosticAt = now();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
diagnosticAt = normalized.lastTimestamp();
|
|
84
|
+
if (code !== "clock-failed") {
|
|
85
|
+
recordDiagnostic(Object.freeze({
|
|
86
|
+
code: "clock-failed",
|
|
87
|
+
at: diagnosticAt,
|
|
88
|
+
applicationId: normalized.applicationId,
|
|
89
|
+
portId,
|
|
90
|
+
message: "Configured clock failed; the triggering state was not committed.",
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const diagnostic = Object.freeze({
|
|
95
|
+
code,
|
|
96
|
+
at: diagnosticAt,
|
|
97
|
+
applicationId: normalized.applicationId,
|
|
98
|
+
portId,
|
|
99
|
+
...(detail.slice === undefined ? {} : { slice: detail.slice }),
|
|
100
|
+
...(detail.revision === undefined ? {} : { revision: detail.revision }),
|
|
101
|
+
message,
|
|
102
|
+
});
|
|
103
|
+
recordDiagnostic(diagnostic);
|
|
104
|
+
}
|
|
105
|
+
function recordDiagnostic(diagnostic) {
|
|
106
|
+
diagnostics.push(diagnostic);
|
|
107
|
+
if (diagnostics.length > normalized.maxDiagnostics)
|
|
108
|
+
diagnostics.splice(0, diagnostics.length - normalized.maxDiagnostics);
|
|
109
|
+
emit({ type: "diagnostic", diagnostic });
|
|
110
|
+
}
|
|
111
|
+
function receive(attachment, foreign) {
|
|
112
|
+
if (disposed || !attachment.active || attachments.get(attachment.port.id) !== attachment)
|
|
113
|
+
return;
|
|
114
|
+
let input;
|
|
115
|
+
try {
|
|
116
|
+
input = normalizeInput(snapshotForeignData(foreign));
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
diagnose("stale-update", attachment.port.id, "Renderer emitted an invalid state envelope.");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (input.sequence <= attachment.lastSequence) {
|
|
123
|
+
diagnose("stale-update", attachment.port.id, "Renderer sequence is stale or out of order.", {
|
|
124
|
+
slice: input.slice,
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
attachment.lastSequence = input.sequence;
|
|
129
|
+
if (input.causeRevision !== undefined && input.causeRevision <= attachment.lastDeliveredRevision) {
|
|
130
|
+
diagnose("loop-suppressed", attachment.port.id, "Renderer acknowledged an applied revision; echo suppressed.", {
|
|
131
|
+
slice: input.slice,
|
|
132
|
+
revision: input.causeRevision,
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const mapping = attachment.port.mappings[input.slice];
|
|
137
|
+
if (mapping.inbound === "unsupported") {
|
|
138
|
+
diagnose("unsupported-origin", attachment.port.id, mapping.message, { slice: input.slice });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (input.slice === "camera" || input.slice === "time") {
|
|
142
|
+
const key = `${attachment.port.id}\0${input.slice}`;
|
|
143
|
+
if (pending.has(key)) {
|
|
144
|
+
diagnose("coalesced", attachment.port.id, "High-frequency update replaced by a newer final state.", {
|
|
145
|
+
slice: input.slice,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
pending.set(key, { attachment, input });
|
|
149
|
+
scheduleFlush();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
commit(attachment, input);
|
|
153
|
+
}
|
|
154
|
+
function scheduleFlush() {
|
|
155
|
+
if (timer !== undefined || microtaskScheduled)
|
|
156
|
+
return;
|
|
157
|
+
if (normalized.coalesceMs === 0) {
|
|
158
|
+
microtaskScheduled = true;
|
|
159
|
+
queueMicrotask(() => {
|
|
160
|
+
microtaskScheduled = false;
|
|
161
|
+
if (!disposed)
|
|
162
|
+
commitPending();
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
timer = setTimeout(() => {
|
|
167
|
+
timer = undefined;
|
|
168
|
+
if (!disposed)
|
|
169
|
+
commitPending();
|
|
170
|
+
}, normalized.coalesceMs);
|
|
171
|
+
}
|
|
172
|
+
function commitPending() {
|
|
173
|
+
if (timer !== undefined) {
|
|
174
|
+
clearTimeout(timer);
|
|
175
|
+
timer = undefined;
|
|
176
|
+
}
|
|
177
|
+
const batch = [...pending.values()];
|
|
178
|
+
pending.clear();
|
|
179
|
+
for (const item of batch) {
|
|
180
|
+
if (item.attachment.active && attachments.get(item.attachment.port.id) === item.attachment) {
|
|
181
|
+
commit(item.attachment, item.input);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function commit(attachment, input) {
|
|
186
|
+
const previous = values[input.slice];
|
|
187
|
+
const signature = canonicalSignature(input.value, input.identity);
|
|
188
|
+
if (previous && canonicalSignature(previous.value, previous.identity) === signature) {
|
|
189
|
+
diagnose("duplicate-suppressed", attachment.port.id, "Equivalent state is already current.", {
|
|
190
|
+
slice: input.slice,
|
|
191
|
+
revision: previous.revision,
|
|
192
|
+
});
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
let acceptedAt;
|
|
196
|
+
try {
|
|
197
|
+
acceptedAt = now();
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
diagnose("clock-failed", attachment.port.id, "Configured clock failed; state was not committed.", {
|
|
201
|
+
slice: input.slice,
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const nextRevision = revision + 1;
|
|
206
|
+
const envelope = Object.freeze({
|
|
207
|
+
kind: SCENE_STATE_SYNC_KIND,
|
|
208
|
+
version: SCENE_STATE_SYNC_VERSION,
|
|
209
|
+
revision: nextRevision,
|
|
210
|
+
acceptedAt,
|
|
211
|
+
emittedAt: input.emittedAt,
|
|
212
|
+
slice: input.slice,
|
|
213
|
+
value: input.value,
|
|
214
|
+
identity: input.identity,
|
|
215
|
+
origin: Object.freeze({
|
|
216
|
+
applicationId: normalized.applicationId,
|
|
217
|
+
portId: attachment.port.id,
|
|
218
|
+
renderer: attachment.port.renderer,
|
|
219
|
+
}),
|
|
220
|
+
fidelity: attachment.port.mappings[input.slice],
|
|
221
|
+
});
|
|
222
|
+
revision = nextRevision;
|
|
223
|
+
values[input.slice] = envelope;
|
|
224
|
+
const current = snapshot();
|
|
225
|
+
emit({ type: "state", envelope, snapshot: current });
|
|
226
|
+
for (const target of attachments.values()) {
|
|
227
|
+
if (target !== attachment)
|
|
228
|
+
deliver(target, envelope);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function deliver(attachment, envelope) {
|
|
232
|
+
if (!attachment.active)
|
|
233
|
+
return;
|
|
234
|
+
const fidelity = attachment.port.mappings[envelope.slice];
|
|
235
|
+
if (fidelity.outbound === "unsupported") {
|
|
236
|
+
diagnose("unsupported-target", attachment.port.id, fidelity.message, {
|
|
237
|
+
slice: envelope.slice,
|
|
238
|
+
revision: envelope.revision,
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
attachment.lastDeliveredRevision = Math.max(attachment.lastDeliveredRevision, envelope.revision);
|
|
243
|
+
const delivery = Object.freeze({
|
|
244
|
+
envelope,
|
|
245
|
+
target: Object.freeze({
|
|
246
|
+
applicationId: normalized.applicationId,
|
|
247
|
+
portId: attachment.port.id,
|
|
248
|
+
renderer: attachment.port.renderer,
|
|
249
|
+
}),
|
|
250
|
+
fidelity,
|
|
251
|
+
});
|
|
252
|
+
const expectedGeneration = attachment.generation;
|
|
253
|
+
attachment.chain = attachment.chain
|
|
254
|
+
.then(async () => {
|
|
255
|
+
if (disposed ||
|
|
256
|
+
!attachment.active ||
|
|
257
|
+
attachment.generation !== expectedGeneration ||
|
|
258
|
+
attachments.get(attachment.port.id) !== attachment) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
await attachment.port.apply(delivery, attachment.controller.signal);
|
|
262
|
+
})
|
|
263
|
+
.catch(() => {
|
|
264
|
+
if (!disposed && attachment.active) {
|
|
265
|
+
diagnose("apply-failed", attachment.port.id, "Renderer failed to apply synchronized state.", {
|
|
266
|
+
slice: envelope.slice,
|
|
267
|
+
revision: envelope.revision,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
function attach(portInput) {
|
|
273
|
+
ensureLive();
|
|
274
|
+
if (attachments.size >= normalized.maxPorts) {
|
|
275
|
+
throw new HonuaSceneStateSyncError("attach-failed", `Scene state synchronization is limited to ${normalized.maxPorts} ports`);
|
|
276
|
+
}
|
|
277
|
+
const port = normalizePort(portInput);
|
|
278
|
+
if (attachments.has(port.id)) {
|
|
279
|
+
throw new HonuaSceneStateSyncError("duplicate-port", `Scene state port ${port.id} is already attached`);
|
|
280
|
+
}
|
|
281
|
+
const controller = new AbortController();
|
|
282
|
+
const attachment = {
|
|
283
|
+
port,
|
|
284
|
+
controller,
|
|
285
|
+
generation: ++generation,
|
|
286
|
+
unsubscribe: () => undefined,
|
|
287
|
+
active: true,
|
|
288
|
+
lastSequence: -1,
|
|
289
|
+
lastDeliveredRevision: 0,
|
|
290
|
+
chain: Promise.resolve(),
|
|
291
|
+
};
|
|
292
|
+
const synchronousEvents = [];
|
|
293
|
+
let subscribing = true;
|
|
294
|
+
let subscribeFailure;
|
|
295
|
+
try {
|
|
296
|
+
const unsubscribe = port.subscribe((event) => {
|
|
297
|
+
if (!subscribing) {
|
|
298
|
+
receive(attachment, event);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (synchronousEvents.length >= MAX_SYNCHRONOUS_SUBSCRIBE_EVENTS) {
|
|
302
|
+
subscribeFailure ??= new HonuaSceneStateSyncError("attach-failed", `Scene state port ${port.id} exceeded the synchronous subscription event limit`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
synchronousEvents.push(snapshotForeignData(event));
|
|
307
|
+
}
|
|
308
|
+
catch (cause) {
|
|
309
|
+
subscribeFailure ??= new HonuaSceneStateSyncError("attach-failed", `Scene state port ${port.id} emitted invalid synchronous state`, { cause });
|
|
310
|
+
}
|
|
311
|
+
}, controller.signal);
|
|
312
|
+
if (typeof unsubscribe !== "function")
|
|
313
|
+
throw new TypeError("port.subscribe must return an unsubscribe function");
|
|
314
|
+
attachment.unsubscribe = once(unsubscribe);
|
|
315
|
+
if (subscribeFailure)
|
|
316
|
+
throw subscribeFailure;
|
|
317
|
+
attachments.set(port.id, attachment);
|
|
318
|
+
subscribing = false;
|
|
319
|
+
}
|
|
320
|
+
catch (cause) {
|
|
321
|
+
subscribing = false;
|
|
322
|
+
attachment.active = false;
|
|
323
|
+
controller.abort();
|
|
324
|
+
try {
|
|
325
|
+
attachment.unsubscribe();
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// Preserve the attach failure.
|
|
329
|
+
}
|
|
330
|
+
throw new HonuaSceneStateSyncError("attach-failed", `Failed to attach scene state port ${port.id}`, { cause });
|
|
331
|
+
}
|
|
332
|
+
const replayValues = Object.values(values).sort((left, right) => left.revision - right.revision);
|
|
333
|
+
for (const event of synchronousEvents)
|
|
334
|
+
receive(attachment, event);
|
|
335
|
+
for (const envelope of replayValues) {
|
|
336
|
+
deliver(attachment, envelope);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function detach(portIdInput) {
|
|
340
|
+
ensureLive();
|
|
341
|
+
const portId = boundedId(portIdInput, "portId");
|
|
342
|
+
const attachment = attachments.get(portId);
|
|
343
|
+
if (!attachment)
|
|
344
|
+
throw new HonuaSceneStateSyncError("unknown-port", `Scene state port ${portId} is not attached`);
|
|
345
|
+
attachments.delete(portId);
|
|
346
|
+
attachment.active = false;
|
|
347
|
+
attachment.controller.abort();
|
|
348
|
+
for (const [key, item] of pending) {
|
|
349
|
+
if (item.attachment === attachment)
|
|
350
|
+
pending.delete(key);
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
attachment.unsubscribe();
|
|
354
|
+
}
|
|
355
|
+
catch (cause) {
|
|
356
|
+
diagnose("port-detached", portId, "Port detached, but listener cleanup reported a failure.");
|
|
357
|
+
throw new HonuaSceneStateSyncError("dispose-failed", `Failed to detach scene state port ${portId}`, { cause });
|
|
358
|
+
}
|
|
359
|
+
diagnose("port-detached", portId, "Renderer port detached and pending updates were cancelled.");
|
|
360
|
+
}
|
|
361
|
+
function attachAll(ports) {
|
|
362
|
+
ensureLive();
|
|
363
|
+
const capturedPorts = captureDenseArray(ports, HARD_MAX_PORTS, "ports");
|
|
364
|
+
const attached = [];
|
|
365
|
+
try {
|
|
366
|
+
for (const port of capturedPorts) {
|
|
367
|
+
const before = new Set(attachments.keys());
|
|
368
|
+
attach(port);
|
|
369
|
+
const id = [...attachments.keys()].find((candidate) => !before.has(candidate));
|
|
370
|
+
if (id === undefined)
|
|
371
|
+
throw new HonuaSceneStateSyncError("attach-failed", "Port attachment did not register a port");
|
|
372
|
+
attached.push(id);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
catch (cause) {
|
|
376
|
+
const cleanup = [];
|
|
377
|
+
for (const id of attached.reverse()) {
|
|
378
|
+
try {
|
|
379
|
+
detach(id);
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
cleanup.push(error);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
throw new HonuaSceneStateSyncError("attach-failed", "Scene state port group attachment rolled back", {
|
|
386
|
+
cause: cleanup.length === 0 ? cause : new AggregateError([cause, ...cleanup]),
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function subscribe(listener) {
|
|
391
|
+
ensureLive();
|
|
392
|
+
if (typeof listener !== "function")
|
|
393
|
+
throw new HonuaSceneStateSyncError("invalid-input", "listener must be a function");
|
|
394
|
+
listeners.add(listener);
|
|
395
|
+
return once(() => listeners.delete(listener));
|
|
396
|
+
}
|
|
397
|
+
async function flush() {
|
|
398
|
+
ensureLive();
|
|
399
|
+
commitPending();
|
|
400
|
+
for (;;) {
|
|
401
|
+
const current = [...attachments.values()];
|
|
402
|
+
const chains = current.map((attachment) => attachment.chain);
|
|
403
|
+
await Promise.all(chains);
|
|
404
|
+
if (current.every((attachment, index) => attachment.chain === chains[index]))
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function dispose() {
|
|
409
|
+
if (disposed)
|
|
410
|
+
return;
|
|
411
|
+
disposed = true;
|
|
412
|
+
if (timer !== undefined)
|
|
413
|
+
clearTimeout(timer);
|
|
414
|
+
timer = undefined;
|
|
415
|
+
pending.clear();
|
|
416
|
+
removeAbortListener?.();
|
|
417
|
+
listeners.clear();
|
|
418
|
+
const failures = [];
|
|
419
|
+
for (const attachment of attachments.values()) {
|
|
420
|
+
attachment.active = false;
|
|
421
|
+
attachment.controller.abort();
|
|
422
|
+
try {
|
|
423
|
+
attachment.unsubscribe();
|
|
424
|
+
}
|
|
425
|
+
catch (cause) {
|
|
426
|
+
failures.push(cause);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
attachments.clear();
|
|
430
|
+
if (failures.length > 0) {
|
|
431
|
+
throw new HonuaSceneStateSyncError("dispose-failed", "Scene state synchronization disposed with cleanup failures", {
|
|
432
|
+
cause: new AggregateError(failures),
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const synchronizer = {
|
|
437
|
+
get disposed() {
|
|
438
|
+
return disposed;
|
|
439
|
+
},
|
|
440
|
+
get portIds() {
|
|
441
|
+
return Object.freeze([...attachments.keys()]);
|
|
442
|
+
},
|
|
443
|
+
get snapshot() {
|
|
444
|
+
ensureLive();
|
|
445
|
+
return snapshot();
|
|
446
|
+
},
|
|
447
|
+
attach,
|
|
448
|
+
attachAll,
|
|
449
|
+
detach,
|
|
450
|
+
subscribe,
|
|
451
|
+
flush,
|
|
452
|
+
dispose,
|
|
453
|
+
};
|
|
454
|
+
try {
|
|
455
|
+
if (normalized.signal) {
|
|
456
|
+
const abort = () => {
|
|
457
|
+
try {
|
|
458
|
+
synchronizer.dispose();
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
// Cancellation still transitions to disposed after cleanup attempts.
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
removeAbortListener = normalized.signal.subscribe(abort);
|
|
465
|
+
if (normalized.signal.isAborted())
|
|
466
|
+
abort();
|
|
467
|
+
}
|
|
468
|
+
if (!disposed && normalized.ports.length > 0)
|
|
469
|
+
attachAll(normalized.ports);
|
|
470
|
+
}
|
|
471
|
+
catch (cause) {
|
|
472
|
+
try {
|
|
473
|
+
synchronizer.dispose();
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
// Preserve the construction failure after best-effort transactional cleanup.
|
|
477
|
+
}
|
|
478
|
+
throw cause;
|
|
479
|
+
}
|
|
480
|
+
return synchronizer;
|
|
481
|
+
}
|
|
482
|
+
/** Explicit default mapping truth for a renderer; callers may narrow any slice. */
|
|
483
|
+
export function defaultSceneStateSyncMappings(renderer) {
|
|
484
|
+
if (renderer !== "maplibre" && renderer !== "cesium" && renderer !== "custom")
|
|
485
|
+
throw new HonuaSceneStateSyncError("invalid-input", "renderer is invalid");
|
|
486
|
+
const exact = (code, message) => Object.freeze({ inbound: "exact", outbound: "exact", code, message });
|
|
487
|
+
const equivalent = (code, message) => Object.freeze({ inbound: "equivalent", outbound: "equivalent", code, message });
|
|
488
|
+
const unsupported = (slice) => Object.freeze({
|
|
489
|
+
inbound: "unsupported",
|
|
490
|
+
outbound: "unsupported",
|
|
491
|
+
code: `custom-${slice}-unsupported`,
|
|
492
|
+
message: `Custom renderer support for ${slice} must be declared explicitly.`,
|
|
493
|
+
});
|
|
494
|
+
if (renderer === "custom") {
|
|
495
|
+
return Object.freeze(Object.fromEntries(SCENE_STATE_SYNC_SLICES.map((slice) => [slice, unsupported(slice)])));
|
|
496
|
+
}
|
|
497
|
+
const mappings = {
|
|
498
|
+
camera: equivalent(renderer === "maplibre" ? "maplibre-2d-camera" : "cesium-3d-camera", renderer === "maplibre"
|
|
499
|
+
? "MapLibre center/zoom/pitch is equivalent but cannot preserve Cesium roll or globe horizon."
|
|
500
|
+
: "Cesium globe camera is equivalent to the canonical camera and may include 3D orientation unavailable in 2D."),
|
|
501
|
+
selection: exact("source-qualified-selection", "Source-qualified feature identity maps exactly."),
|
|
502
|
+
filters: exact("protocol-neutral-filter", "Protocol-neutral filter clauses map exactly."),
|
|
503
|
+
time: renderer === "maplibre"
|
|
504
|
+
? equivalent("maplibre-time", "MapLibre has no native clock; the adapter applies canonical application time.")
|
|
505
|
+
: exact("cesium-clock", "Cesium clock maps to canonical application time."),
|
|
506
|
+
detail: exact("source-qualified-detail", "Detail identity and bounded attributes map exactly."),
|
|
507
|
+
attribution: exact("attribution-ids", "Credential-free attribution identifiers map exactly."),
|
|
508
|
+
realtime: exact("realtime-status", "Freshness, status, cursor presence, and lag map exactly."),
|
|
509
|
+
};
|
|
510
|
+
return Object.freeze(mappings);
|
|
511
|
+
}
|
|
512
|
+
function normalizeOptions(options) {
|
|
513
|
+
if (!isRecord(options))
|
|
514
|
+
throw new HonuaSceneStateSyncError("invalid-input", "options must be an object");
|
|
515
|
+
const applicationId = boundedId(dataProperty(options, "applicationId"), "applicationId");
|
|
516
|
+
const coalesceRaw = optionalDataProperty(options, "coalesceMs");
|
|
517
|
+
const maxPortsRaw = optionalDataProperty(options, "maxPorts");
|
|
518
|
+
const maxDiagnosticsRaw = optionalDataProperty(options, "maxDiagnostics");
|
|
519
|
+
const portsRaw = optionalDataProperty(options, "ports");
|
|
520
|
+
const nowRaw = optionalDataProperty(options, "now");
|
|
521
|
+
const signalRaw = optionalDataProperty(options, "signal");
|
|
522
|
+
const signal = signalRaw === undefined ? undefined : normalizeAbortSignal(signalRaw);
|
|
523
|
+
const coalesceMs = coalesceRaw ?? 16;
|
|
524
|
+
if (typeof coalesceMs !== "number" || !Number.isSafeInteger(coalesceMs) || coalesceMs < 0 || coalesceMs > 1_000)
|
|
525
|
+
throw new HonuaSceneStateSyncError("invalid-input", "coalesceMs must be an integer between 0 and 1000");
|
|
526
|
+
const maxPorts = boundedPositiveInteger(maxPortsRaw ?? DEFAULT_MAX_PORTS, "maxPorts", HARD_MAX_PORTS);
|
|
527
|
+
const maxDiagnostics = boundedPositiveInteger(maxDiagnosticsRaw ?? DEFAULT_MAX_DIAGNOSTICS, "maxDiagnostics", HARD_MAX_DIAGNOSTICS);
|
|
528
|
+
if (nowRaw !== undefined && typeof nowRaw !== "function")
|
|
529
|
+
throw new HonuaSceneStateSyncError("invalid-input", "now must be a function");
|
|
530
|
+
const ports = portsRaw === undefined ? [] : captureDenseArray(portsRaw, HARD_MAX_PORTS, "ports");
|
|
531
|
+
const foreignNow = nowRaw === undefined ? () => new Date().toISOString() : nowRaw;
|
|
532
|
+
let lastTimestamp;
|
|
533
|
+
try {
|
|
534
|
+
lastTimestamp = canonicalTimestamp(foreignNow.call(undefined), "clock");
|
|
535
|
+
}
|
|
536
|
+
catch (cause) {
|
|
537
|
+
throw new HonuaSceneStateSyncError("invalid-input", "clock must return a canonical ISO timestamp", { cause });
|
|
538
|
+
}
|
|
539
|
+
const now = () => {
|
|
540
|
+
lastTimestamp = canonicalTimestamp(foreignNow.call(undefined), "clock");
|
|
541
|
+
return lastTimestamp;
|
|
542
|
+
};
|
|
543
|
+
return Object.freeze({
|
|
544
|
+
applicationId,
|
|
545
|
+
coalesceMs,
|
|
546
|
+
maxPorts,
|
|
547
|
+
maxDiagnostics,
|
|
548
|
+
ports: Object.freeze(ports),
|
|
549
|
+
now,
|
|
550
|
+
lastTimestamp: () => lastTimestamp,
|
|
551
|
+
signal,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
function normalizePort(foreign) {
|
|
555
|
+
if (!isRecord(foreign))
|
|
556
|
+
throw new HonuaSceneStateSyncError("invalid-input", "port must be an object");
|
|
557
|
+
const id = boundedId(dataProperty(foreign, "id"), "port.id");
|
|
558
|
+
const renderer = dataProperty(foreign, "renderer");
|
|
559
|
+
if (renderer !== "maplibre" && renderer !== "cesium" && renderer !== "custom")
|
|
560
|
+
throw new HonuaSceneStateSyncError("invalid-input", "port.renderer is invalid");
|
|
561
|
+
const mappings = normalizeMappings(dataProperty(foreign, "mappings"));
|
|
562
|
+
const subscribe = callback(foreign, "subscribe");
|
|
563
|
+
const apply = callback(foreign, "apply");
|
|
564
|
+
return Object.freeze({
|
|
565
|
+
id,
|
|
566
|
+
renderer,
|
|
567
|
+
mappings,
|
|
568
|
+
subscribe: subscribe,
|
|
569
|
+
apply: apply,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
function normalizeMappings(foreign) {
|
|
573
|
+
if (!isRecord(foreign))
|
|
574
|
+
throw new HonuaSceneStateSyncError("invalid-input", "port.mappings must be an object");
|
|
575
|
+
const result = Object.create(null);
|
|
576
|
+
for (const slice of SCENE_STATE_SYNC_SLICES) {
|
|
577
|
+
const value = dataProperty(foreign, slice);
|
|
578
|
+
if (!isRecord(value))
|
|
579
|
+
throw new HonuaSceneStateSyncError("invalid-input", `mapping ${slice} must be an object`);
|
|
580
|
+
const inbound = dataProperty(value, "inbound");
|
|
581
|
+
const outbound = dataProperty(value, "outbound");
|
|
582
|
+
if (!isFidelity(inbound) || !isFidelity(outbound))
|
|
583
|
+
throw new HonuaSceneStateSyncError("invalid-input", `mapping ${slice} fidelity is invalid`);
|
|
584
|
+
result[slice] = Object.freeze({
|
|
585
|
+
inbound,
|
|
586
|
+
outbound,
|
|
587
|
+
code: boundedId(dataProperty(value, "code"), `mapping ${slice}.code`),
|
|
588
|
+
message: boundedText(dataProperty(value, "message"), `mapping ${slice}.message`, 512),
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
return Object.freeze(result);
|
|
592
|
+
}
|
|
593
|
+
function normalizeInput(foreign) {
|
|
594
|
+
if (!isRecord(foreign))
|
|
595
|
+
throw new HonuaSceneStateSyncError("invalid-input", "state input must be an object");
|
|
596
|
+
if (dataProperty(foreign, "kind") !== SCENE_STATE_SYNC_KIND ||
|
|
597
|
+
dataProperty(foreign, "version") !== SCENE_STATE_SYNC_VERSION)
|
|
598
|
+
throw new HonuaSceneStateSyncError("invalid-input", "state input discriminator is invalid");
|
|
599
|
+
const slice = dataProperty(foreign, "slice");
|
|
600
|
+
if (!SCENE_STATE_SYNC_SLICES.includes(slice))
|
|
601
|
+
throw new HonuaSceneStateSyncError("invalid-input", "state input slice is invalid");
|
|
602
|
+
const sequence = nonNegativeInteger(dataProperty(foreign, "sequence"), "sequence");
|
|
603
|
+
const causeRaw = optionalDataProperty(foreign, "causeRevision");
|
|
604
|
+
const causeRevision = causeRaw === undefined ? undefined : positiveInteger(causeRaw, "causeRevision");
|
|
605
|
+
const identity = normalizeIdentity(dataProperty(foreign, "identity"));
|
|
606
|
+
const value = normalizeValue(slice, dataProperty(foreign, "value"));
|
|
607
|
+
return Object.freeze({
|
|
608
|
+
kind: SCENE_STATE_SYNC_KIND,
|
|
609
|
+
version: SCENE_STATE_SYNC_VERSION,
|
|
610
|
+
sequence,
|
|
611
|
+
emittedAt: canonicalTimestamp(dataProperty(foreign, "emittedAt"), "emittedAt"),
|
|
612
|
+
slice,
|
|
613
|
+
value,
|
|
614
|
+
identity,
|
|
615
|
+
...(causeRevision === undefined ? {} : { causeRevision }),
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
function normalizeIdentity(foreign) {
|
|
619
|
+
if (!isRecord(foreign))
|
|
620
|
+
throw new HonuaSceneStateSyncError("invalid-input", "identity must be an object");
|
|
621
|
+
const optional = (key) => {
|
|
622
|
+
const value = optionalDataProperty(foreign, key);
|
|
623
|
+
return value === undefined ? undefined : boundedId(value, `identity.${key}`);
|
|
624
|
+
};
|
|
625
|
+
const fingerprint = optionalDataProperty(foreign, "planFingerprint");
|
|
626
|
+
const schemaVersion = optional("schemaVersion");
|
|
627
|
+
const sourceVersion = optional("sourceVersion");
|
|
628
|
+
const planId = optional("planId");
|
|
629
|
+
if (fingerprint !== undefined && (typeof fingerprint !== "string" || !/^sha256:[a-f0-9]{64}$/.test(fingerprint)))
|
|
630
|
+
throw new HonuaSceneStateSyncError("invalid-input", "identity.planFingerprint is invalid");
|
|
631
|
+
return Object.freeze({
|
|
632
|
+
sourceId: boundedId(dataProperty(foreign, "sourceId"), "identity.sourceId"),
|
|
633
|
+
...(schemaVersion === undefined ? {} : { schemaVersion }),
|
|
634
|
+
...(sourceVersion === undefined ? {} : { sourceVersion }),
|
|
635
|
+
...(planId === undefined ? {} : { planId }),
|
|
636
|
+
...(fingerprint === undefined ? {} : { planFingerprint: fingerprint }),
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
function normalizeValue(slice, foreign) {
|
|
640
|
+
switch (slice) {
|
|
641
|
+
case "camera":
|
|
642
|
+
if (foreign === undefined)
|
|
643
|
+
return undefined;
|
|
644
|
+
return normalizeCamera(foreign);
|
|
645
|
+
case "selection":
|
|
646
|
+
return normalizeSelection(foreign);
|
|
647
|
+
case "filters":
|
|
648
|
+
return normalizeFilters(foreign);
|
|
649
|
+
case "time":
|
|
650
|
+
return normalizeTime(foreign);
|
|
651
|
+
case "detail":
|
|
652
|
+
return normalizeDetail(foreign);
|
|
653
|
+
case "attribution":
|
|
654
|
+
return normalizeAttribution(foreign);
|
|
655
|
+
case "realtime":
|
|
656
|
+
return normalizeRealtime(foreign);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
function normalizeCamera(foreign) {
|
|
660
|
+
if (!isRecord(foreign))
|
|
661
|
+
throw new HonuaSceneStateSyncError("invalid-input", "camera must be an object");
|
|
662
|
+
const optionalNumber = (key) => {
|
|
663
|
+
const value = optionalDataProperty(foreign, key);
|
|
664
|
+
return value === undefined ? undefined : finiteNumber(value, `camera.${key}`);
|
|
665
|
+
};
|
|
666
|
+
const longitude = finiteNumber(dataProperty(foreign, "longitude"), "camera.longitude");
|
|
667
|
+
const latitude = finiteNumber(dataProperty(foreign, "latitude"), "camera.latitude");
|
|
668
|
+
const height = finiteNumber(dataProperty(foreign, "height"), "camera.height");
|
|
669
|
+
if (longitude < -180 || longitude > 180 || latitude < -90 || latitude > 90 || height < 0)
|
|
670
|
+
throw new HonuaSceneStateSyncError("invalid-input", "camera coordinates are outside supported bounds");
|
|
671
|
+
const heading = optionalNumber("heading");
|
|
672
|
+
const pitch = optionalNumber("pitch");
|
|
673
|
+
const roll = optionalNumber("roll");
|
|
674
|
+
return Object.freeze({
|
|
675
|
+
longitude,
|
|
676
|
+
latitude,
|
|
677
|
+
height,
|
|
678
|
+
...(heading === undefined ? {} : { heading }),
|
|
679
|
+
...(pitch === undefined ? {} : { pitch }),
|
|
680
|
+
...(roll === undefined ? {} : { roll }),
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
function normalizeSelection(foreign) {
|
|
684
|
+
if (!Array.isArray(foreign) || foreign.length > MAX_SELECTION)
|
|
685
|
+
throw new HonuaSceneStateSyncError("invalid-input", `selection must contain at most ${MAX_SELECTION} targets`);
|
|
686
|
+
return Object.freeze(foreign.map((target, index) => {
|
|
687
|
+
if (typeof target === "string")
|
|
688
|
+
return boundedFeatureId(target, `selection[${index}]`);
|
|
689
|
+
if (typeof target === "number") {
|
|
690
|
+
if (!Number.isFinite(target))
|
|
691
|
+
throw new HonuaSceneStateSyncError("invalid-input", `selection[${index}] is invalid`);
|
|
692
|
+
return Object.is(target, -0) ? 0 : target;
|
|
693
|
+
}
|
|
694
|
+
if (!isRecord(target))
|
|
695
|
+
throw new HonuaSceneStateSyncError("invalid-input", `selection[${index}] is invalid`);
|
|
696
|
+
const sourceLayer = optionalDataProperty(target, "sourceLayer");
|
|
697
|
+
const id = dataProperty(target, "id");
|
|
698
|
+
if (typeof id !== "string" && (typeof id !== "number" || !Number.isFinite(id)))
|
|
699
|
+
throw new HonuaSceneStateSyncError("invalid-input", `selection[${index}].id is invalid`);
|
|
700
|
+
return Object.freeze({
|
|
701
|
+
sourceId: boundedId(dataProperty(target, "sourceId"), `selection[${index}].sourceId`),
|
|
702
|
+
id: typeof id === "string" ? boundedFeatureId(id, `selection[${index}].id`) : Object.is(id, -0) ? 0 : id,
|
|
703
|
+
...(sourceLayer === undefined
|
|
704
|
+
? {}
|
|
705
|
+
: { sourceLayer: boundedId(sourceLayer, `selection[${index}].sourceLayer`) }),
|
|
706
|
+
});
|
|
707
|
+
}));
|
|
708
|
+
}
|
|
709
|
+
function normalizeFilters(foreign) {
|
|
710
|
+
if (!isRecord(foreign))
|
|
711
|
+
throw new HonuaSceneStateSyncError("invalid-input", "filters must be an object");
|
|
712
|
+
const result = Object.create(null);
|
|
713
|
+
let count = 0;
|
|
714
|
+
for (const key in foreign) {
|
|
715
|
+
if (count >= MAX_FILTERS)
|
|
716
|
+
throw new HonuaSceneStateSyncError("invalid-input", `filters exceed ${MAX_FILTERS}`);
|
|
717
|
+
count += 1;
|
|
718
|
+
const clause = dataProperty(foreign, key);
|
|
719
|
+
if (!isRecord(clause))
|
|
720
|
+
throw new HonuaSceneStateSyncError("invalid-input", `filter ${key} is invalid`);
|
|
721
|
+
const appliesTo = optionalDataProperty(clause, "appliesTo");
|
|
722
|
+
if (appliesTo !== undefined && (!Array.isArray(appliesTo) || appliesTo.length > 128))
|
|
723
|
+
throw new HonuaSceneStateSyncError("invalid-input", `filter ${key}.appliesTo is invalid`);
|
|
724
|
+
const value = optionalDataProperty(clause, "value");
|
|
725
|
+
result[boundedId(key, "filter id")] = Object.freeze({
|
|
726
|
+
field: boundedId(dataProperty(clause, "field"), `filter ${key}.field`),
|
|
727
|
+
operator: normalizeFilterOperator(dataProperty(clause, "operator")),
|
|
728
|
+
...(value === undefined ? {} : { value: snapshotJson(value) }),
|
|
729
|
+
...(appliesTo === undefined
|
|
730
|
+
? {}
|
|
731
|
+
: { appliesTo: Object.freeze(appliesTo.map((id) => boundedId(id, "source id"))) }),
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
return Object.freeze(result);
|
|
735
|
+
}
|
|
736
|
+
function normalizeTime(foreign) {
|
|
737
|
+
if (!isRecord(foreign))
|
|
738
|
+
throw new HonuaSceneStateSyncError("invalid-input", "time must be an object");
|
|
739
|
+
const iso = (key) => {
|
|
740
|
+
const value = optionalDataProperty(foreign, key);
|
|
741
|
+
return value === undefined ? undefined : canonicalTimestamp(value, `time.${key}`);
|
|
742
|
+
};
|
|
743
|
+
const playing = optionalDataProperty(foreign, "playing");
|
|
744
|
+
const progress = optionalDataProperty(foreign, "progress");
|
|
745
|
+
if (playing !== undefined && typeof playing !== "boolean")
|
|
746
|
+
throw new HonuaSceneStateSyncError("invalid-input", "time.playing is invalid");
|
|
747
|
+
if (progress !== undefined &&
|
|
748
|
+
(typeof progress !== "number" || !Number.isFinite(progress) || progress < 0 || progress > 1))
|
|
749
|
+
throw new HonuaSceneStateSyncError("invalid-input", "time.progress is invalid");
|
|
750
|
+
const currentTime = iso("currentTime");
|
|
751
|
+
const startTime = iso("startTime");
|
|
752
|
+
const endTime = iso("endTime");
|
|
753
|
+
return Object.freeze({
|
|
754
|
+
...(currentTime === undefined ? {} : { currentTime }),
|
|
755
|
+
...(startTime === undefined ? {} : { startTime }),
|
|
756
|
+
...(endTime === undefined ? {} : { endTime }),
|
|
757
|
+
...(playing === undefined ? {} : { playing }),
|
|
758
|
+
...(progress === undefined ? {} : { progress }),
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
function normalizeDetail(foreign) {
|
|
762
|
+
if (!isRecord(foreign))
|
|
763
|
+
throw new HonuaSceneStateSyncError("invalid-input", "detail must be an object");
|
|
764
|
+
const target = optionalDataProperty(foreign, "target");
|
|
765
|
+
const attributes = optionalDataProperty(foreign, "attributes");
|
|
766
|
+
const selection = target === undefined ? undefined : normalizeSelection([target])[0];
|
|
767
|
+
const optionalId = (key) => {
|
|
768
|
+
const value = optionalDataProperty(foreign, key);
|
|
769
|
+
return value === undefined ? undefined : boundedId(value, `detail.${key}`);
|
|
770
|
+
};
|
|
771
|
+
const featureId = optionalDataProperty(foreign, "featureId");
|
|
772
|
+
if (featureId !== undefined &&
|
|
773
|
+
typeof featureId !== "string" &&
|
|
774
|
+
(typeof featureId !== "number" || !Number.isFinite(featureId)))
|
|
775
|
+
throw new HonuaSceneStateSyncError("invalid-input", "detail.featureId is invalid");
|
|
776
|
+
const status = optionalDataProperty(foreign, "status");
|
|
777
|
+
if (status !== undefined && !["empty", "loading", "ready", "stale", "error"].includes(status))
|
|
778
|
+
throw new HonuaSceneStateSyncError("invalid-input", "detail.status is invalid");
|
|
779
|
+
const title = optionalDataProperty(foreign, "title");
|
|
780
|
+
const sourceId = optionalId("sourceId");
|
|
781
|
+
return Object.freeze({
|
|
782
|
+
...(selection === undefined ? {} : { target: selection }),
|
|
783
|
+
...(sourceId === undefined ? {} : { sourceId }),
|
|
784
|
+
...(featureId === undefined
|
|
785
|
+
? {}
|
|
786
|
+
: {
|
|
787
|
+
featureId: typeof featureId === "string"
|
|
788
|
+
? boundedFeatureId(featureId, "detail.featureId")
|
|
789
|
+
: Object.is(featureId, -0)
|
|
790
|
+
? 0
|
|
791
|
+
: featureId,
|
|
792
|
+
}),
|
|
793
|
+
...(title === undefined ? {} : { title: boundedText(title, "detail.title", 512) }),
|
|
794
|
+
...(status === undefined ? {} : { status: status }),
|
|
795
|
+
...(attributes === undefined ? {} : { attributes: snapshotJson(attributes) }),
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
function normalizeAttribution(foreign) {
|
|
799
|
+
if (!isRecord(foreign))
|
|
800
|
+
throw new HonuaSceneStateSyncError("invalid-input", "attribution must be an object");
|
|
801
|
+
const ids = dataProperty(foreign, "ids");
|
|
802
|
+
if (!Array.isArray(ids) || ids.length > MAX_ATTRIBUTIONS)
|
|
803
|
+
throw new HonuaSceneStateSyncError("invalid-input", `attribution ids exceed ${MAX_ATTRIBUTIONS}`);
|
|
804
|
+
return Object.freeze({ ids: Object.freeze([...new Set(ids.map((id) => boundedId(id, "attribution id")))].sort()) });
|
|
805
|
+
}
|
|
806
|
+
function normalizeRealtime(foreign) {
|
|
807
|
+
if (!isRecord(foreign))
|
|
808
|
+
throw new HonuaSceneStateSyncError("invalid-input", "realtime must be an object");
|
|
809
|
+
const status = dataProperty(foreign, "status");
|
|
810
|
+
const freshness = dataProperty(foreign, "freshness");
|
|
811
|
+
const cursorPresent = dataProperty(foreign, "cursorPresent");
|
|
812
|
+
if (!isRealtimeStatus(status) ||
|
|
813
|
+
!["fresh", "stale", "unknown"].includes(freshness) ||
|
|
814
|
+
typeof cursorPresent !== "boolean")
|
|
815
|
+
throw new HonuaSceneStateSyncError("invalid-input", "realtime state is invalid");
|
|
816
|
+
const observedAt = optionalDataProperty(foreign, "observedAt");
|
|
817
|
+
const lagMs = optionalDataProperty(foreign, "lagMs");
|
|
818
|
+
if (lagMs !== undefined && (!Number.isSafeInteger(lagMs) || lagMs < 0))
|
|
819
|
+
throw new HonuaSceneStateSyncError("invalid-input", "realtime.lagMs is invalid");
|
|
820
|
+
return Object.freeze({
|
|
821
|
+
status,
|
|
822
|
+
freshness: freshness,
|
|
823
|
+
cursorPresent,
|
|
824
|
+
...(observedAt === undefined ? {} : { observedAt: canonicalTimestamp(observedAt, "realtime.observedAt") }),
|
|
825
|
+
...(lagMs === undefined ? {} : { lagMs: lagMs }),
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
function snapshotJson(foreign) {
|
|
829
|
+
const budget = { nodes: 0 };
|
|
830
|
+
const visit = (value, depth, ancestors) => {
|
|
831
|
+
budget.nodes += 1;
|
|
832
|
+
if (budget.nodes > MAX_JSON_NODES || depth > MAX_JSON_DEPTH)
|
|
833
|
+
throw new HonuaSceneStateSyncError("invalid-input", "JSON value is too complex");
|
|
834
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
835
|
+
return value;
|
|
836
|
+
if (typeof value === "number") {
|
|
837
|
+
if (!Number.isFinite(value))
|
|
838
|
+
throw new HonuaSceneStateSyncError("invalid-input", "JSON numbers must be finite");
|
|
839
|
+
return Object.is(value, -0) ? 0 : value;
|
|
840
|
+
}
|
|
841
|
+
if (!isRecord(value) && !Array.isArray(value))
|
|
842
|
+
throw new HonuaSceneStateSyncError("invalid-input", "value is not JSON-compatible");
|
|
843
|
+
if (ancestors.has(value))
|
|
844
|
+
throw new HonuaSceneStateSyncError("invalid-input", "JSON value is cyclic");
|
|
845
|
+
ancestors.add(value);
|
|
846
|
+
if (Array.isArray(value)) {
|
|
847
|
+
if (value.length > MAX_JSON_PROPERTIES)
|
|
848
|
+
throw new HonuaSceneStateSyncError("invalid-input", "JSON array is too wide");
|
|
849
|
+
const result = value.map((item) => visit(item, depth + 1, ancestors));
|
|
850
|
+
ancestors.delete(value);
|
|
851
|
+
return Object.freeze(result);
|
|
852
|
+
}
|
|
853
|
+
const result = Object.create(null);
|
|
854
|
+
let count = 0;
|
|
855
|
+
for (const key in value) {
|
|
856
|
+
if (count >= MAX_JSON_PROPERTIES)
|
|
857
|
+
throw new HonuaSceneStateSyncError("invalid-input", "JSON object is too wide");
|
|
858
|
+
count += 1;
|
|
859
|
+
result[key] = visit(dataProperty(value, key), depth + 1, ancestors);
|
|
860
|
+
}
|
|
861
|
+
ancestors.delete(value);
|
|
862
|
+
return Object.freeze(result);
|
|
863
|
+
};
|
|
864
|
+
return visit(foreign, 0, new WeakSet());
|
|
865
|
+
}
|
|
866
|
+
function canonicalSignature(value, identity) {
|
|
867
|
+
return stableStringify({ value, identity });
|
|
868
|
+
}
|
|
869
|
+
function stableStringify(value) {
|
|
870
|
+
if (value === null || typeof value !== "object")
|
|
871
|
+
return JSON.stringify(value);
|
|
872
|
+
if (Array.isArray(value))
|
|
873
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
874
|
+
return `{${Object.keys(value)
|
|
875
|
+
.sort()
|
|
876
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
|
|
877
|
+
.join(",")}}`;
|
|
878
|
+
}
|
|
879
|
+
function snapshotForeignData(foreign) {
|
|
880
|
+
const budget = { nodes: 0, stringUnits: 0 };
|
|
881
|
+
const ancestors = new WeakSet();
|
|
882
|
+
const visit = (value, depth) => {
|
|
883
|
+
budget.nodes += 1;
|
|
884
|
+
if (budget.nodes > MAX_FOREIGN_NODES || depth > MAX_JSON_DEPTH)
|
|
885
|
+
throw new HonuaSceneStateSyncError("invalid-input", "Renderer envelope is too complex");
|
|
886
|
+
if (typeof value === "string") {
|
|
887
|
+
budget.stringUnits += value.length;
|
|
888
|
+
if (value.length > MAX_FOREIGN_STRING || budget.stringUnits > MAX_FOREIGN_STRING_TOTAL)
|
|
889
|
+
throw new HonuaSceneStateSyncError("invalid-input", "Renderer envelope strings exceed the bounded limit");
|
|
890
|
+
return value;
|
|
891
|
+
}
|
|
892
|
+
if (value === null || value === undefined || typeof value === "boolean" || typeof value === "number")
|
|
893
|
+
return value;
|
|
894
|
+
if (typeof value !== "object")
|
|
895
|
+
throw new HonuaSceneStateSyncError("invalid-input", "Renderer envelope must contain data only");
|
|
896
|
+
if (ancestors.has(value))
|
|
897
|
+
throw new HonuaSceneStateSyncError("invalid-input", "Renderer envelope is cyclic");
|
|
898
|
+
ancestors.add(value);
|
|
899
|
+
try {
|
|
900
|
+
if (Array.isArray(value)) {
|
|
901
|
+
const items = captureDenseArray(value, MAX_FOREIGN_ARRAY, "renderer envelope array");
|
|
902
|
+
return Object.freeze(items.map((item) => visit(item, depth + 1)));
|
|
903
|
+
}
|
|
904
|
+
let prototype;
|
|
905
|
+
let keys;
|
|
906
|
+
try {
|
|
907
|
+
prototype = Object.getPrototypeOf(value);
|
|
908
|
+
keys = Reflect.ownKeys(value);
|
|
909
|
+
}
|
|
910
|
+
catch (cause) {
|
|
911
|
+
throw new HonuaSceneStateSyncError("invalid-input", "Renderer envelope object could not be captured", {
|
|
912
|
+
cause,
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
916
|
+
throw new HonuaSceneStateSyncError("invalid-input", "Renderer envelope records must be plain objects");
|
|
917
|
+
if (keys.length > MAX_JSON_PROPERTIES || keys.some((key) => typeof key === "symbol"))
|
|
918
|
+
throw new HonuaSceneStateSyncError("invalid-input", "Renderer envelope object keys are invalid");
|
|
919
|
+
const result = Object.create(null);
|
|
920
|
+
for (const key of keys)
|
|
921
|
+
result[key] = visit(dataProperty(value, key), depth + 1);
|
|
922
|
+
return Object.freeze(result);
|
|
923
|
+
}
|
|
924
|
+
finally {
|
|
925
|
+
ancestors.delete(value);
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
return visit(foreign, 0);
|
|
929
|
+
}
|
|
930
|
+
function captureDenseArray(foreign, maximum, label) {
|
|
931
|
+
if (!Array.isArray(foreign))
|
|
932
|
+
throw new HonuaSceneStateSyncError("invalid-input", `${label} must be an array`);
|
|
933
|
+
let keys;
|
|
934
|
+
let length;
|
|
935
|
+
try {
|
|
936
|
+
keys = Reflect.ownKeys(foreign);
|
|
937
|
+
length = Reflect.getOwnPropertyDescriptor(foreign, "length")?.value;
|
|
938
|
+
}
|
|
939
|
+
catch (cause) {
|
|
940
|
+
throw new HonuaSceneStateSyncError("invalid-input", `${label} could not be captured`, { cause });
|
|
941
|
+
}
|
|
942
|
+
if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || length > maximum)
|
|
943
|
+
throw new HonuaSceneStateSyncError("invalid-input", `${label} exceeds its bounded length`);
|
|
944
|
+
if (keys.some((key) => typeof key === "symbol"))
|
|
945
|
+
throw new HonuaSceneStateSyncError("invalid-input", `${label} cannot contain symbol properties`);
|
|
946
|
+
const allowed = new Set(["length", ...Array.from({ length }, (_, index) => String(index))]);
|
|
947
|
+
if (keys.some((key) => !allowed.has(key)) || keys.length !== length + 1)
|
|
948
|
+
throw new HonuaSceneStateSyncError("invalid-input", `${label} must be dense and contain only indexed data`);
|
|
949
|
+
const result = [];
|
|
950
|
+
for (let index = 0; index < length; index += 1)
|
|
951
|
+
result.push(dataProperty(foreign, String(index)));
|
|
952
|
+
return result;
|
|
953
|
+
}
|
|
954
|
+
function dataProperty(input, key) {
|
|
955
|
+
let descriptor;
|
|
956
|
+
try {
|
|
957
|
+
descriptor = Reflect.getOwnPropertyDescriptor(input, key);
|
|
958
|
+
}
|
|
959
|
+
catch (cause) {
|
|
960
|
+
throw new HonuaSceneStateSyncError("invalid-input", `${key} could not be captured`, { cause });
|
|
961
|
+
}
|
|
962
|
+
if (!descriptor || descriptor.get || descriptor.set)
|
|
963
|
+
throw new HonuaSceneStateSyncError("invalid-input", `${key} must be a data property`);
|
|
964
|
+
return descriptor.value;
|
|
965
|
+
}
|
|
966
|
+
function optionalDataProperty(input, key) {
|
|
967
|
+
let descriptor;
|
|
968
|
+
try {
|
|
969
|
+
descriptor = Reflect.getOwnPropertyDescriptor(input, key);
|
|
970
|
+
}
|
|
971
|
+
catch (cause) {
|
|
972
|
+
throw new HonuaSceneStateSyncError("invalid-input", `${key} could not be captured`, { cause });
|
|
973
|
+
}
|
|
974
|
+
if (!descriptor)
|
|
975
|
+
return undefined;
|
|
976
|
+
if (descriptor.get || descriptor.set)
|
|
977
|
+
throw new HonuaSceneStateSyncError("invalid-input", `${key} must be a data property`);
|
|
978
|
+
return descriptor.value;
|
|
979
|
+
}
|
|
980
|
+
function callback(input, key) {
|
|
981
|
+
const value = dataProperty(input, key);
|
|
982
|
+
if (typeof value !== "function")
|
|
983
|
+
throw new HonuaSceneStateSyncError("invalid-input", `${key} must be a function`);
|
|
984
|
+
return value.bind(input);
|
|
985
|
+
}
|
|
986
|
+
function normalizeAbortSignal(foreign) {
|
|
987
|
+
if (!isRecord(foreign))
|
|
988
|
+
throw new HonuaSceneStateSyncError("invalid-input", "signal must be an AbortSignal");
|
|
989
|
+
let add;
|
|
990
|
+
let remove;
|
|
991
|
+
try {
|
|
992
|
+
add = Reflect.get(foreign, "addEventListener");
|
|
993
|
+
remove = Reflect.get(foreign, "removeEventListener");
|
|
994
|
+
}
|
|
995
|
+
catch (cause) {
|
|
996
|
+
throw new HonuaSceneStateSyncError("invalid-input", "signal methods could not be captured", { cause });
|
|
997
|
+
}
|
|
998
|
+
if (typeof add !== "function" || typeof remove !== "function")
|
|
999
|
+
throw new HonuaSceneStateSyncError("invalid-input", "signal must be an AbortSignal");
|
|
1000
|
+
const addEventListener = add.bind(foreign);
|
|
1001
|
+
const removeEventListener = remove.bind(foreign);
|
|
1002
|
+
const isAborted = () => {
|
|
1003
|
+
let value;
|
|
1004
|
+
try {
|
|
1005
|
+
value = Reflect.get(foreign, "aborted");
|
|
1006
|
+
}
|
|
1007
|
+
catch (cause) {
|
|
1008
|
+
throw new HonuaSceneStateSyncError("invalid-input", "signal.aborted could not be read", { cause });
|
|
1009
|
+
}
|
|
1010
|
+
if (typeof value !== "boolean")
|
|
1011
|
+
throw new HonuaSceneStateSyncError("invalid-input", "signal.aborted must be boolean");
|
|
1012
|
+
return value;
|
|
1013
|
+
};
|
|
1014
|
+
isAborted();
|
|
1015
|
+
return Object.freeze({
|
|
1016
|
+
isAborted,
|
|
1017
|
+
subscribe(listener) {
|
|
1018
|
+
try {
|
|
1019
|
+
addEventListener("abort", listener, { once: true });
|
|
1020
|
+
}
|
|
1021
|
+
catch (cause) {
|
|
1022
|
+
throw new HonuaSceneStateSyncError("invalid-input", "signal abort listener could not be attached", { cause });
|
|
1023
|
+
}
|
|
1024
|
+
return once(() => {
|
|
1025
|
+
try {
|
|
1026
|
+
removeEventListener("abort", listener);
|
|
1027
|
+
}
|
|
1028
|
+
catch {
|
|
1029
|
+
// Cancellation listener cleanup is best effort for foreign signals.
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
},
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
function boundedId(value, label) {
|
|
1036
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 256 || !SAFE_ID.test(value))
|
|
1037
|
+
throw new HonuaSceneStateSyncError("invalid-input", `${label} must be a bounded credential-free identifier`);
|
|
1038
|
+
return value;
|
|
1039
|
+
}
|
|
1040
|
+
function boundedFeatureId(value, label) {
|
|
1041
|
+
if (value.length === 0 ||
|
|
1042
|
+
value.length > 256 ||
|
|
1043
|
+
[...value].some((character) => {
|
|
1044
|
+
const code = character.charCodeAt(0);
|
|
1045
|
+
return code <= 31 || code === 127;
|
|
1046
|
+
}))
|
|
1047
|
+
throw new HonuaSceneStateSyncError("invalid-input", `${label} must be a bounded feature identifier`);
|
|
1048
|
+
return value;
|
|
1049
|
+
}
|
|
1050
|
+
function boundedText(value, label, max) {
|
|
1051
|
+
if (typeof value !== "string" ||
|
|
1052
|
+
value.length === 0 ||
|
|
1053
|
+
value.length > max ||
|
|
1054
|
+
[...value].some((character) => {
|
|
1055
|
+
const code = character.charCodeAt(0);
|
|
1056
|
+
return code <= 31 || code === 127;
|
|
1057
|
+
}))
|
|
1058
|
+
throw new HonuaSceneStateSyncError("invalid-input", `${label} must be bounded text without control characters`);
|
|
1059
|
+
return value;
|
|
1060
|
+
}
|
|
1061
|
+
function canonicalTimestamp(value, label) {
|
|
1062
|
+
if (typeof value !== "string" || !Number.isFinite(Date.parse(value)) || new Date(value).toISOString() !== value)
|
|
1063
|
+
throw new HonuaSceneStateSyncError("invalid-input", `${label} must be a canonical ISO timestamp`);
|
|
1064
|
+
return value;
|
|
1065
|
+
}
|
|
1066
|
+
function finiteNumber(value, label) {
|
|
1067
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
1068
|
+
throw new HonuaSceneStateSyncError("invalid-input", `${label} must be finite`);
|
|
1069
|
+
return value;
|
|
1070
|
+
}
|
|
1071
|
+
function positiveInteger(value, label) {
|
|
1072
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
1073
|
+
throw new HonuaSceneStateSyncError("invalid-input", `${label} must be a positive safe integer`);
|
|
1074
|
+
return value;
|
|
1075
|
+
}
|
|
1076
|
+
function boundedPositiveInteger(value, label, maximum) {
|
|
1077
|
+
const normalized = positiveInteger(value, label);
|
|
1078
|
+
if (normalized > maximum)
|
|
1079
|
+
throw new HonuaSceneStateSyncError("invalid-input", `${label} must not exceed ${maximum}`);
|
|
1080
|
+
return normalized;
|
|
1081
|
+
}
|
|
1082
|
+
function nonNegativeInteger(value, label) {
|
|
1083
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
1084
|
+
throw new HonuaSceneStateSyncError("invalid-input", `${label} must be a non-negative safe integer`);
|
|
1085
|
+
return value;
|
|
1086
|
+
}
|
|
1087
|
+
function normalizeFilterOperator(value) {
|
|
1088
|
+
const operators = [
|
|
1089
|
+
"=",
|
|
1090
|
+
"!=",
|
|
1091
|
+
"<",
|
|
1092
|
+
"<=",
|
|
1093
|
+
">",
|
|
1094
|
+
">=",
|
|
1095
|
+
"in",
|
|
1096
|
+
"not-in",
|
|
1097
|
+
"between",
|
|
1098
|
+
"like",
|
|
1099
|
+
"is-null",
|
|
1100
|
+
"is-not-null",
|
|
1101
|
+
];
|
|
1102
|
+
if (!operators.includes(value))
|
|
1103
|
+
throw new HonuaSceneStateSyncError("invalid-input", "filter operator is invalid");
|
|
1104
|
+
return value;
|
|
1105
|
+
}
|
|
1106
|
+
function isRealtimeStatus(value) {
|
|
1107
|
+
return ["idle", "connecting", "connected", "reconnecting", "disconnected", "error"].includes(value);
|
|
1108
|
+
}
|
|
1109
|
+
function isFidelity(value) {
|
|
1110
|
+
return value === "exact" || value === "equivalent" || value === "unsupported";
|
|
1111
|
+
}
|
|
1112
|
+
function isRecord(value) {
|
|
1113
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1114
|
+
}
|
|
1115
|
+
function once(callback) {
|
|
1116
|
+
let called = false;
|
|
1117
|
+
return () => {
|
|
1118
|
+
if (called)
|
|
1119
|
+
return;
|
|
1120
|
+
called = true;
|
|
1121
|
+
callback();
|
|
1122
|
+
};
|
|
1123
|
+
}
|