@honua/react 0.1.2-beta.0 → 0.1.4-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/contract/discovery.d.ts +34 -0
- package/contract/discovery.d.ts.map +1 -1
- package/contract/index.d.ts +7 -5
- package/contract/index.d.ts.map +1 -1
- package/contract/index.js +3 -2
- package/contract/pmtiles.d.ts +10 -1
- package/contract/pmtiles.d.ts.map +1 -1
- package/contract/pmtiles.js +10 -2
- package/contract/protocol-module.d.ts +84 -9
- package/contract/protocol-module.d.ts.map +1 -1
- package/contract/query-filter-fes.d.ts +18 -0
- package/contract/query-filter-fes.d.ts.map +1 -0
- package/contract/query-filter-fes.js +122 -0
- package/contract/query-filter.d.ts +300 -0
- package/contract/query-filter.d.ts.map +1 -0
- package/contract/query-filter.js +883 -0
- package/contract/schema.d.ts +12 -0
- package/contract/schema.d.ts.map +1 -1
- package/contract/schema.js +34 -0
- package/contract/source-capability-support.d.ts.map +1 -1
- package/contract/source-capability-support.js +15 -3
- package/contract/source.d.ts +45 -25
- package/contract/source.d.ts.map +1 -1
- package/contract/source.js +719 -479
- package/contract/types.d.ts +87 -18
- package/contract/types.d.ts.map +1 -1
- package/contract/widget-source.d.ts.map +1 -1
- package/core/client.d.ts +45 -21
- package/core/client.d.ts.map +1 -1
- package/core/client.js +224 -46
- package/core/error-classifications.d.ts +6 -0
- package/core/error-classifications.d.ts.map +1 -1
- package/core/error-classifications.js +6 -0
- package/core/error-code-registry.d.ts +6 -0
- package/core/error-code-registry.d.ts.map +1 -1
- package/core/error-code-registry.js +6 -0
- package/core/error-envelope.js +2 -0
- package/core/errors.d.ts +1 -1
- package/core/errors.js +1 -1
- package/core/geoparquet-sql.d.ts +68 -4
- package/core/geoparquet-sql.d.ts.map +1 -1
- package/core/geoparquet-sql.js +325 -8
- package/core/odata-write-codec.d.ts.map +1 -1
- package/core/odata-write-codec.js +0 -1
- package/core/odata.d.ts +6 -0
- package/core/odata.d.ts.map +1 -1
- package/core/odata.js +6 -1
- package/core/ogc-processes.d.ts +182 -25
- package/core/ogc-processes.d.ts.map +1 -1
- package/core/ogc-processes.js +602 -30
- package/core/process-runner.d.ts +8 -1
- package/core/process-runner.d.ts.map +1 -1
- package/core/process-runner.js +1 -0
- package/core/protocol-transport.d.ts +2 -0
- package/core/protocol-transport.d.ts.map +1 -1
- package/core/stac.d.ts +31 -0
- package/core/stac.d.ts.map +1 -1
- package/core/stac.js +136 -19
- package/core/surfaces.d.ts.map +1 -1
- package/core/surfaces.js +273 -68
- package/core/types.d.ts +123 -9
- package/core/types.d.ts.map +1 -1
- package/core/wfs-axis-order.d.ts +18 -0
- package/core/wfs-axis-order.d.ts.map +1 -0
- package/core/wfs-axis-order.js +108 -0
- package/core/wfs-capabilities.d.ts +65 -24
- package/core/wfs-capabilities.d.ts.map +1 -1
- package/core/wfs-capabilities.js +237 -27
- package/core/wfs-filter.d.ts +26 -3
- package/core/wfs-filter.d.ts.map +1 -1
- package/core/wfs-filter.js +48 -53
- package/core/wfs-protocol-error.d.ts +12 -0
- package/core/wfs-protocol-error.d.ts.map +1 -0
- package/core/wfs-protocol-error.js +13 -0
- package/core/wfs.d.ts +53 -1
- package/core/wfs.d.ts.map +1 -1
- package/core/wfs.js +251 -29
- package/core/wms-feature-info.d.ts +92 -0
- package/core/wms-feature-info.d.ts.map +1 -0
- package/core/wms-feature-info.js +285 -0
- package/core/wms.d.ts +9 -0
- package/core/wms.d.ts.map +1 -1
- package/core/wms.js +17 -5
- package/esri-compat/event-bus.d.ts +20 -0
- package/esri-compat/event-bus.d.ts.map +1 -1
- package/esri-compat/feature-filter.d.ts +46 -1
- package/esri-compat/feature-filter.d.ts.map +1 -1
- package/esri-compat/feature-filter.js +49 -0
- package/esri-compat/geojson-layer.d.ts +4 -2
- package/esri-compat/geojson-layer.d.ts.map +1 -1
- package/esri-compat/geojson-layer.js +4 -2
- package/esri-compat/identity-manager.js +2 -2
- package/esri-compat/imagery-layer.d.ts +2 -1
- package/esri-compat/imagery-layer.d.ts.map +1 -1
- package/esri-compat/imagery-layer.js +2 -1
- package/esri-compat/layer-list.d.ts +8 -1
- package/esri-compat/layer-list.d.ts.map +1 -1
- package/esri-compat/layer-list.js +11 -4
- package/esri-compat/legend.d.ts.map +1 -1
- package/esri-compat/legend.js +1 -1
- package/esri-compat/locator.d.ts +297 -0
- package/esri-compat/locator.d.ts.map +1 -0
- package/esri-compat/locator.js +579 -0
- package/esri-compat/map-image-layer.d.ts.map +1 -1
- package/esri-compat/map-view.d.ts.map +1 -1
- package/esri-compat/map-view.js +1 -1
- package/esri-compat/scene-view.d.ts.map +1 -1
- package/esri-compat/time-slider.d.ts +29 -0
- package/esri-compat/time-slider.d.ts.map +1 -1
- package/esri-compat/time-slider.js +117 -0
- package/esri-compat/wfs-layer.d.ts +2 -1
- package/esri-compat/wfs-layer.d.ts.map +1 -1
- package/esri-compat/wfs-layer.js +2 -1
- package/esri-compat/widget-host.d.ts +42 -6
- package/esri-compat/widget-host.d.ts.map +1 -1
- package/esri-compat/widget-host.js +99 -10
- package/generated-app/projection.d.ts.map +1 -1
- package/generated-app/runtime.d.ts.map +1 -1
- package/map/data-to-map-bridge.d.ts +1 -1
- package/map/data-to-map-bridge.d.ts.map +1 -1
- package/map/data-to-map-bridge.js +1 -2
- package/map/honua-map.d.ts.map +1 -1
- package/map/honua-map.js +1 -1
- package/map/index.d.ts +2 -0
- package/map/index.d.ts.map +1 -1
- package/map/index.js +2 -0
- package/map/realtime-reconciliation-adapter.d.ts +98 -0
- package/map/realtime-reconciliation-adapter.d.ts.map +1 -0
- package/map/realtime-reconciliation-adapter.js +84 -0
- package/map/temporal-playback.d.ts +25 -0
- package/map/temporal-playback.d.ts.map +1 -1
- package/map/temporal-playback.js +29 -1
- package/package.json +4 -3
- package/query-planner/canonical-filter.d.ts +30 -0
- package/query-planner/canonical-filter.d.ts.map +1 -0
- package/query-planner/canonical-filter.js +30 -0
- package/query-planner/columnar-plan.d.ts +63 -0
- package/query-planner/columnar-plan.d.ts.map +1 -0
- package/query-planner/columnar-plan.js +94 -0
- package/query-planner/diagnostics.d.ts +3 -3
- package/query-planner/diagnostics.d.ts.map +1 -1
- package/query-planner/diagnostics.js +17 -4
- package/query-planner/duckdb.d.ts.map +1 -1
- package/query-planner/duckdb.js +16 -2
- package/query-planner/execution-context.d.ts.map +1 -1
- package/query-planner/execution-context.js +3 -1
- package/query-planner/executor.d.ts.map +1 -1
- package/query-planner/executor.js +7 -1
- package/query-planner/geoservices.d.ts.map +1 -1
- package/query-planner/geoservices.js +24 -5
- package/query-planner/grpc.d.ts.map +1 -1
- package/query-planner/grpc.js +29 -5
- package/query-planner/index.d.ts +20 -5
- package/query-planner/index.d.ts.map +1 -1
- package/query-planner/index.js +16 -3
- package/query-planner/ir.d.ts +6 -1
- package/query-planner/ir.d.ts.map +1 -1
- package/query-planner/ir.js +63 -1
- package/query-planner/odata-protocol-module.d.ts +54 -0
- package/query-planner/odata-protocol-module.d.ts.map +1 -0
- package/query-planner/odata-protocol-module.js +199 -0
- package/query-planner/odata-v1.d.ts +23 -0
- package/query-planner/odata-v1.d.ts.map +1 -0
- package/query-planner/odata-v1.js +202 -0
- package/query-planner/odata.d.ts +0 -3
- package/query-planner/odata.d.ts.map +1 -1
- package/query-planner/odata.js +0 -143
- package/query-planner/ogc-features.d.ts.map +1 -1
- package/query-planner/ogc-features.js +23 -1
- package/query-planner/planner.d.ts +2 -2
- package/query-planner/planner.d.ts.map +1 -1
- package/query-planner/planner.js +146 -27
- package/query-planner/representation.d.ts +59 -0
- package/query-planner/representation.d.ts.map +1 -0
- package/query-planner/representation.js +140 -0
- package/query-planner/types.d.ts +153 -3
- package/query-planner/types.d.ts.map +1 -1
- package/query-planner/wfs-protocol-module.d.ts +57 -0
- package/query-planner/wfs-protocol-module.d.ts.map +1 -0
- package/query-planner/wfs-protocol-module.js +731 -0
- package/query-planner/wfs-v1.d.ts +41 -0
- package/query-planner/wfs-v1.d.ts.map +1 -0
- package/query-planner/wfs-v1.js +366 -0
- package/query-planner/wfs.d.ts +1 -3
- package/query-planner/wfs.d.ts.map +1 -1
- package/query-planner/wfs.js +1 -70
- package/react/honua-map.d.ts +3 -1
- package/react/honua-map.d.ts.map +1 -1
- package/react/honua-map.js +11 -1
- package/react/hooks.d.ts +1 -1
- package/react/hooks.js +1 -1
- package/react/selection.d.ts.map +1 -1
- package/react/selection.js +0 -0
- package/react/use-mounted-source.d.ts +1 -1
- package/react/use-mounted-source.js +1 -1
- package/realtime/checkpoint-store.d.ts +206 -0
- package/realtime/checkpoint-store.d.ts.map +1 -0
- package/realtime/checkpoint-store.js +504 -0
- package/realtime/contract.d.ts +142 -0
- package/realtime/contract.d.ts.map +1 -0
- package/realtime/contract.js +154 -0
- package/realtime/exploration.d.ts +16 -0
- package/realtime/exploration.d.ts.map +1 -0
- package/realtime/exploration.js +32 -0
- package/realtime/honua-server.d.ts +131 -0
- package/realtime/honua-server.d.ts.map +1 -0
- package/realtime/honua-server.js +302 -0
- package/realtime/index.d.ts +39 -0
- package/realtime/index.d.ts.map +1 -0
- package/realtime/index.js +26 -0
- 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/projections.d.ts +30 -0
- package/realtime/projections.d.ts.map +1 -0
- package/realtime/projections.js +51 -0
- package/realtime/reconciliation.d.ts +245 -0
- package/realtime/reconciliation.d.ts.map +1 -0
- package/realtime/reconciliation.js +377 -0
- package/realtime/reducer.d.ts +14 -0
- package/realtime/reducer.d.ts.map +1 -0
- package/realtime/reducer.js +351 -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 +23 -0
- package/realtime/sse.d.ts.map +1 -0
- package/realtime/sse.js +147 -0
- package/realtime/store.d.ts +18 -0
- package/realtime/store.d.ts.map +1 -0
- package/realtime/store.js +63 -0
- package/realtime/types.d.ts +190 -0
- package/realtime/types.d.ts.map +1 -0
- package/realtime/types.js +10 -0
- 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 +28 -0
- package/realtime/wire.d.ts.map +1 -0
- package/realtime/wire.js +73 -0
- package/runtime/index.d.ts +6 -2
- package/runtime/index.d.ts.map +1 -1
- package/runtime/index.js +3 -1
- package/runtime/load-package.d.ts +24 -0
- package/runtime/load-package.d.ts.map +1 -1
- package/runtime/load-package.js +45 -1
- package/runtime/maplibre-renderer.d.ts +11 -0
- package/runtime/maplibre-renderer.d.ts.map +1 -1
- package/runtime/maplibre-renderer.js +32 -7
- package/runtime/offline-region-protocol.d.ts +73 -0
- package/runtime/offline-region-protocol.d.ts.map +1 -0
- package/runtime/offline-region-protocol.js +63 -0
- package/runtime/offline-region-style.d.ts +78 -0
- package/runtime/offline-region-style.d.ts.map +1 -0
- package/runtime/offline-region-style.js +147 -0
- package/runtime/pmtiles-protocol.d.ts +7 -8
- package/runtime/pmtiles-protocol.d.ts.map +1 -1
- package/runtime/pmtiles-protocol.js +19 -66
- package/runtime/protocol-registry.d.ts +55 -0
- package/runtime/protocol-registry.d.ts.map +1 -0
- package/runtime/protocol-registry.js +103 -0
- package/runtime/runtime.d.ts +2 -0
- package/runtime/runtime.d.ts.map +1 -1
- package/runtime/runtime.js +6 -1
- package/studio/index.d.ts +24 -7
- package/studio/index.d.ts.map +1 -1
- package/studio/index.js +21 -7
- package/studio/lifecycle-client.d.ts +164 -0
- package/studio/lifecycle-client.d.ts.map +1 -0
- package/studio/lifecycle-client.js +299 -0
- package/studio/lifecycle-errors.d.ts +91 -0
- package/studio/lifecycle-errors.d.ts.map +1 -0
- package/studio/lifecycle-errors.js +77 -0
- package/studio/lifecycle-types.d.ts +321 -0
- package/studio/lifecycle-types.d.ts.map +1 -0
- package/studio/lifecycle-types.js +22 -0
- package/webmap/convert-label.js +0 -1
- package/webmap/convert-symbol.d.ts.map +1 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot, delta, cursor, resume, and plan-identity contract surface for
|
|
3
|
+
* `@honua/sdk-js/realtime` (issue #556).
|
|
4
|
+
*
|
|
5
|
+
* The resumable delivery gate in `resumable.ts` already implements the
|
|
6
|
+
* durable-checkpoint state machine: versioned snapshot/delta/cursor
|
|
7
|
+
* envelopes, monotonic-sequence ordering and deduplication, delete
|
|
8
|
+
* tombstones (`reducer.ts`), and mandatory resnapshot on gap, expiry, or
|
|
9
|
+
* scope change. This module closes the three remaining contract gaps that
|
|
10
|
+
* keep that machinery a *design* rather than an ad hoc convention:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Plan identity** — {@link RealtimeResumeContextV1.queryFingerprint} was
|
|
13
|
+
* previously any caller-supplied string. {@link realtimePlanFingerprint}
|
|
14
|
+
* and {@link assertRealtimePlanIdentity} bind it to the query planner's
|
|
15
|
+
* canonical, credential-free `QueryExecutionPlan.fingerprint` — the same
|
|
16
|
+
* content hash `hashQueryPlan`/`persistedPlanSnapshot` establish once,
|
|
17
|
+
* with full protocol-compiler re-verification, when a plan is first
|
|
18
|
+
* constructed (`explainQuery`) or reloaded (`parseQueryPlan`). That
|
|
19
|
+
* fingerprint already folds in `capabilityPolicy` and `fallback`, so
|
|
20
|
+
* binding to it also carries policy identity per REQ-003 without a
|
|
21
|
+
* second fingerprint field. This module reads the already-trusted field
|
|
22
|
+
* rather than importing `hashQueryPlan` itself: plan *integrity* is the
|
|
23
|
+
* query planner's trust boundary, established once at construction time,
|
|
24
|
+
* and re-deriving it here would pull every protocol compiler
|
|
25
|
+
* (DuckDB/gRPC/GeoServices/OGC/OData/WFS — several hundred KB) into the
|
|
26
|
+
* `@honua/sdk-js/realtime` bundle for a check that is already done.
|
|
27
|
+
* 2. **Authority** — {@link deriveRealtimeContractAuthority} projects the
|
|
28
|
+
* resumable gate's `phase` into one explicit
|
|
29
|
+
* `replaying | live | stale | terminal` state plus an `authoritative`
|
|
30
|
+
* flag, so consumers stop inferring trust from reason codes.
|
|
31
|
+
* 3. **Deterministic, redacted serialization** — {@link serializeRealtimeCheckpoint}
|
|
32
|
+
* and {@link redactRealtimeCheckpoint} give a canonical (sorted-key) JSON
|
|
33
|
+
* form for durable storage and a form safe for logs/telemetry that never
|
|
34
|
+
* carries a raw cursor, watermark, or delta-token value.
|
|
35
|
+
*
|
|
36
|
+
* @module
|
|
37
|
+
*/
|
|
38
|
+
import { canonicalStringify, sha256, toJsonValue } from "../query-planner/canonical.js";
|
|
39
|
+
import { HonuaRealtimeResumeError, } from "./resumable.js";
|
|
40
|
+
/**
|
|
41
|
+
* Canonical plan-identity fingerprint for
|
|
42
|
+
* {@link RealtimeResumeContextV1.queryFingerprint}: the accepted plan's own
|
|
43
|
+
* trusted `fingerprint` field (`hashQueryPlan(plan) === plan.fingerprint`
|
|
44
|
+
* for every plan produced by `explainQuery`/`parseQueryPlan`). Named and
|
|
45
|
+
* exported so call sites bind resume identity to the plan by construction
|
|
46
|
+
* instead of by convention, without importing the query planner's
|
|
47
|
+
* protocol-compiler graph into `@honua/sdk-js/realtime`. Only
|
|
48
|
+
* `QueryExecutionPlan["fingerprint"]` is type-imported; nothing from
|
|
49
|
+
* `query-planner/planner.js` is bundled here.
|
|
50
|
+
*/
|
|
51
|
+
export function realtimePlanFingerprint(plan) {
|
|
52
|
+
return plan.fingerprint;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Assert that a resume context's `queryFingerprint` matches the accepted
|
|
56
|
+
* plan's fingerprint. Throws a `"query-changed"`
|
|
57
|
+
* {@link HonuaRealtimeResumeError} — the same taxonomy
|
|
58
|
+
* {@link evaluateRealtimeCheckpoint} uses for a checkpoint whose bound query
|
|
59
|
+
* identity no longer matches — when they diverge, so a subscription can
|
|
60
|
+
* never resume against a plan other than the one it was accepted for. This
|
|
61
|
+
* checks identity, not plan authenticity: trust that `plan.fingerprint`
|
|
62
|
+
* matches the plan's actual content is established once, by the query
|
|
63
|
+
* planner, when the plan is constructed or parsed.
|
|
64
|
+
*/
|
|
65
|
+
export function assertRealtimePlanIdentity(context, plan) {
|
|
66
|
+
const expected = realtimePlanFingerprint(plan);
|
|
67
|
+
if (context.queryFingerprint !== expected) {
|
|
68
|
+
throw new HonuaRealtimeResumeError("query-changed", `Realtime resume context queryFingerprint does not match the accepted plan fingerprint (expected "${expected}", received "${context.queryFingerprint}").`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Derive one explicit {@link RealtimeContractAuthority} from a resumable
|
|
73
|
+
* gate's state. Pure and side-effect free: call it after every
|
|
74
|
+
* `enqueue`/`requireResnapshot`/`close` transition to render connection
|
|
75
|
+
* chrome without re-deriving trust from `phase`/`reason` combinations.
|
|
76
|
+
*/
|
|
77
|
+
export function deriveRealtimeContractAuthority(resumable, options = {}) {
|
|
78
|
+
const ageMs = checkpointAgeMs(resumable.checkpoint, options.now);
|
|
79
|
+
switch (resumable.phase) {
|
|
80
|
+
case "awaiting-snapshot":
|
|
81
|
+
case "resuming":
|
|
82
|
+
case "resnapshot-required":
|
|
83
|
+
return freeze({ state: "replaying", authoritative: false, ...(ageMs !== undefined ? { ageMs } : {}) });
|
|
84
|
+
case "error":
|
|
85
|
+
case "closed":
|
|
86
|
+
return freeze({
|
|
87
|
+
state: "terminal",
|
|
88
|
+
authoritative: resumable.checkpoint !== undefined,
|
|
89
|
+
...(ageMs !== undefined ? { ageMs } : {}),
|
|
90
|
+
});
|
|
91
|
+
case "live": {
|
|
92
|
+
const stale = ageMs !== undefined &&
|
|
93
|
+
typeof options.staleAfterMs === "number" &&
|
|
94
|
+
Number.isFinite(options.staleAfterMs) &&
|
|
95
|
+
ageMs > options.staleAfterMs;
|
|
96
|
+
return freeze({
|
|
97
|
+
state: stale ? "stale" : "live",
|
|
98
|
+
authoritative: true,
|
|
99
|
+
...(ageMs !== undefined ? { ageMs } : {}),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function checkpointAgeMs(checkpoint, now) {
|
|
105
|
+
if (!checkpoint || typeof now !== "number" || !Number.isFinite(now))
|
|
106
|
+
return undefined;
|
|
107
|
+
const savedAt = Date.parse(checkpoint.savedAt);
|
|
108
|
+
if (!Number.isFinite(savedAt))
|
|
109
|
+
return undefined;
|
|
110
|
+
return Math.max(0, now - savedAt);
|
|
111
|
+
}
|
|
112
|
+
export function redactRealtimeCheckpoint(checkpoint) {
|
|
113
|
+
return freeze({
|
|
114
|
+
kind: checkpoint.kind,
|
|
115
|
+
version: checkpoint.version,
|
|
116
|
+
context: checkpoint.context,
|
|
117
|
+
resume: freeze({
|
|
118
|
+
sequence: checkpoint.resume.sequence,
|
|
119
|
+
...(checkpoint.resume.cursor !== undefined ? { cursor: redactPosition("cursor", checkpoint.resume.cursor) } : {}),
|
|
120
|
+
...(checkpoint.resume.watermark !== undefined
|
|
121
|
+
? { watermark: redactPosition("watermark", checkpoint.resume.watermark) }
|
|
122
|
+
: {}),
|
|
123
|
+
...(checkpoint.resume.timestamp !== undefined ? { timestamp: checkpoint.resume.timestamp } : {}),
|
|
124
|
+
...(checkpoint.resume.deltaToken !== undefined
|
|
125
|
+
? { deltaToken: redactPosition("deltaToken", checkpoint.resume.deltaToken) }
|
|
126
|
+
: {}),
|
|
127
|
+
}),
|
|
128
|
+
recentEventIdCount: checkpoint.recentEventIds.length,
|
|
129
|
+
savedAt: checkpoint.savedAt,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function redactPosition(field, value) {
|
|
133
|
+
return sha256(`honua-realtime-checkpoint-redaction:v1:${field}:${value}`);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Canonical (sorted-key) JSON serialization of the full durable checkpoint,
|
|
137
|
+
* for persistence where the caller owns access control. Two checkpoints
|
|
138
|
+
* with the same accepted content always serialize identically, regardless
|
|
139
|
+
* of property insertion order — required for content-addressed storage and
|
|
140
|
+
* for stable diffing across processes.
|
|
141
|
+
*/
|
|
142
|
+
export function serializeRealtimeCheckpoint(checkpoint) {
|
|
143
|
+
return canonicalStringify(toJsonValue(checkpoint));
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Canonical, redacted JSON serialization safe for logs and telemetry
|
|
147
|
+
* sinks — never a raw cursor, watermark, or delta-token value.
|
|
148
|
+
*/
|
|
149
|
+
export function serializeRedactedRealtimeCheckpoint(checkpoint) {
|
|
150
|
+
return canonicalStringify(toJsonValue(redactRealtimeCheckpoint(checkpoint)));
|
|
151
|
+
}
|
|
152
|
+
function freeze(value) {
|
|
153
|
+
return Object.freeze(value);
|
|
154
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realtime helpers for linked exploration views.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import type { SourceId } from "../contract/types.js";
|
|
7
|
+
import type { ExplorationViewController, FeatureSelectionTarget } from "../exploration/types.js";
|
|
8
|
+
import type { RealtimeFeatureState } from "./types.js";
|
|
9
|
+
export interface ReconcileRealtimeSelectionOptions {
|
|
10
|
+
readonly sourceId?: SourceId;
|
|
11
|
+
/** Remove selections when the record is absent from live state. @default true */
|
|
12
|
+
readonly requireLiveRecord?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function filterRealtimeSelection<TFeature>(selection: ReadonlyArray<FeatureSelectionTarget>, state: RealtimeFeatureState<TFeature>, options?: ReconcileRealtimeSelectionOptions): ReadonlyArray<FeatureSelectionTarget>;
|
|
15
|
+
export declare function reconcileRealtimeSelection<TFeature>(view: ExplorationViewController, state: RealtimeFeatureState<TFeature>, options?: ReconcileRealtimeSelectionOptions): void;
|
|
16
|
+
//# sourceMappingURL=exploration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exploration.d.ts","sourceRoot":"","sources":["../../../src/realtime/exploration.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,KAAK,EAAE,yBAAyB,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEjG,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEvD,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B,iFAAiF;IACjF,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAC;CACtC;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAC9C,SAAS,EAAE,aAAa,CAAC,sBAAsB,CAAC,EAChD,KAAK,EAAE,oBAAoB,CAAC,QAAQ,CAAC,EACrC,OAAO,GAAE,iCAAsC,GAC9C,aAAa,CAAC,sBAAsB,CAAC,CAQvC;AAED,wBAAgB,0BAA0B,CAAC,QAAQ,EACjD,IAAI,EAAE,yBAAyB,EAC/B,KAAK,EAAE,oBAAoB,CAAC,QAAQ,CAAC,EACrC,OAAO,GAAE,iCAAsC,GAC9C,IAAI,CAIN"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realtime helpers for linked exploration views.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { isSourceQualifiedSelectionTarget } from "../exploration/selection.js";
|
|
7
|
+
import { realtimeFeatureKey } from "./reducer.js";
|
|
8
|
+
export function filterRealtimeSelection(selection, state, options = {}) {
|
|
9
|
+
const { sourceId, requireLiveRecord = true } = options;
|
|
10
|
+
return selection.filter((target) => {
|
|
11
|
+
const key = selectionKey(target, sourceId);
|
|
12
|
+
if (!key)
|
|
13
|
+
return true;
|
|
14
|
+
if (state.tombstones[key])
|
|
15
|
+
return false;
|
|
16
|
+
return requireLiveRecord ? Boolean(state.records[key]) : true;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export function reconcileRealtimeSelection(view, state, options = {}) {
|
|
20
|
+
const next = filterRealtimeSelection(view.state.selection, state, options);
|
|
21
|
+
if (next.length === view.state.selection.length)
|
|
22
|
+
return;
|
|
23
|
+
view.select(next, { replace: true });
|
|
24
|
+
}
|
|
25
|
+
function selectionKey(target, sourceId) {
|
|
26
|
+
if (isSourceQualifiedSelectionTarget(target)) {
|
|
27
|
+
return realtimeFeatureKey(target.sourceId, target.id);
|
|
28
|
+
}
|
|
29
|
+
if (!sourceId)
|
|
30
|
+
return undefined;
|
|
31
|
+
return realtimeFeatureKey(sourceId, target);
|
|
32
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-party realtime preset for honua-server's
|
|
3
|
+
* `/api/v1/streaming/features` Server-Sent Events endpoint.
|
|
4
|
+
*
|
|
5
|
+
* The default SSE transport encoder emits `sourceId=` / `layerId=` query
|
|
6
|
+
* params and assumes events already use the SDK's `RealtimeFeatureEvent`
|
|
7
|
+
* vocabulary. honua-server instead expects `serviceId=` / `layers=` query
|
|
8
|
+
* params and emits its own feature-change envelopes. This module packages the
|
|
9
|
+
* two hooks (`encodeRequest` + `decodeEvent`) the transport already exposes so
|
|
10
|
+
* consumers do not have to re-implement the same adapter for every app.
|
|
11
|
+
*
|
|
12
|
+
* @example Use the preset options with the SSE transport.
|
|
13
|
+
* ```ts
|
|
14
|
+
* import {
|
|
15
|
+
* createRealtimeServerSentEventsTransport,
|
|
16
|
+
* honuaServerRealtimePreset,
|
|
17
|
+
* } from "@honua/sdk-js/realtime";
|
|
18
|
+
*
|
|
19
|
+
* const transport = createRealtimeServerSentEventsTransport({
|
|
20
|
+
* url: "https://honua.example/api/v1/streaming/features",
|
|
21
|
+
* ...honuaServerRealtimePreset(),
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @example Or use the convenience factory.
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { createHonuaServerRealtimeSubscription } from "@honua/sdk-js/realtime";
|
|
28
|
+
*
|
|
29
|
+
* const transport = createHonuaServerRealtimeSubscription({
|
|
30
|
+
* baseUrl: "https://honua.example",
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @module
|
|
35
|
+
*/
|
|
36
|
+
import type { FeatureId } from "../contract/types.js";
|
|
37
|
+
import type { RealtimeServerSentEventsTransportOptions } from "./sse.js";
|
|
38
|
+
import type { RealtimeFeatureEvent, RealtimeFeatureTransport, RealtimeSubscriptionRequest } from "./types.js";
|
|
39
|
+
/** Default streaming-features path served by honua-server. */
|
|
40
|
+
export declare const HONUA_SERVER_STREAMING_FEATURES_PATH = "/api/v1/streaming/features";
|
|
41
|
+
/**
|
|
42
|
+
* Encode a `RealtimeSubscriptionRequest` into honua-server's
|
|
43
|
+
* `/api/v1/streaming/features` query contract: `serviceId=` (not `sourceId=`)
|
|
44
|
+
* and `layers=` (not `layerId=`). The remaining filter/resume params match the
|
|
45
|
+
* default encoder so cursors, watermarks, and where-clauses keep working.
|
|
46
|
+
*/
|
|
47
|
+
export declare function encodeHonuaServerRealtimeRequest(url: URL, request: RealtimeSubscriptionRequest): URL;
|
|
48
|
+
/**
|
|
49
|
+
* Shape of a single change carried by a honua-server feature-change envelope.
|
|
50
|
+
* honua-server reports the change kind via `op` (`insert` / `update` / `delete`)
|
|
51
|
+
* alongside the affected feature or feature id.
|
|
52
|
+
*/
|
|
53
|
+
export interface HonuaServerFeatureChange<TFeature = unknown> {
|
|
54
|
+
readonly op: "insert" | "update" | "delete" | "snapshot";
|
|
55
|
+
readonly featureId?: FeatureId;
|
|
56
|
+
readonly feature?: TFeature;
|
|
57
|
+
readonly version?: number;
|
|
58
|
+
readonly updatedAt?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Shape of the envelope honua-server writes to the
|
|
62
|
+
* `/api/v1/streaming/features` SSE stream. Each envelope carries the service /
|
|
63
|
+
* layer scope, ordering metadata, and one or more feature changes.
|
|
64
|
+
*/
|
|
65
|
+
export interface HonuaServerFeatureChangeEnvelope<TFeature = unknown> {
|
|
66
|
+
readonly serviceId?: string;
|
|
67
|
+
readonly sourceId?: string;
|
|
68
|
+
readonly layerId?: string | number;
|
|
69
|
+
readonly eventId?: string;
|
|
70
|
+
readonly sequence?: number;
|
|
71
|
+
readonly cursor?: string;
|
|
72
|
+
readonly watermark?: string;
|
|
73
|
+
readonly timestamp?: string;
|
|
74
|
+
readonly deltaToken?: string;
|
|
75
|
+
/** `change` (or `feature-change`) is the default envelope kind. */
|
|
76
|
+
readonly kind?: string;
|
|
77
|
+
readonly changes?: ReadonlyArray<HonuaServerFeatureChange<TFeature>>;
|
|
78
|
+
/** Single-change envelopes inline the change instead of a `changes` array. */
|
|
79
|
+
readonly op?: HonuaServerFeatureChange<TFeature>["op"];
|
|
80
|
+
readonly featureId?: FeatureId;
|
|
81
|
+
readonly objectId?: FeatureId;
|
|
82
|
+
readonly feature?: TFeature;
|
|
83
|
+
/** Current honua-server wire field; normalized to {@link HonuaServerFeatureChange.op}. */
|
|
84
|
+
readonly operation?: Exclude<HonuaServerFeatureChange<TFeature>["op"], "snapshot">;
|
|
85
|
+
/** Current honua-server GeoJSON geometry field. */
|
|
86
|
+
readonly geometry?: unknown;
|
|
87
|
+
/** Current honua-server full attribute snapshot for insert/update events. */
|
|
88
|
+
readonly attributes?: Readonly<Record<string, unknown>>;
|
|
89
|
+
readonly version?: number;
|
|
90
|
+
readonly updatedAt?: string;
|
|
91
|
+
/** Pass-through status / heartbeat / error envelopes. */
|
|
92
|
+
readonly type?: string;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Decode a honua-server `/api/v1/streaming/features` envelope into the SDK's
|
|
96
|
+
* `RealtimeFeatureEvent`. Feature-change envelopes become `delta` events
|
|
97
|
+
* (batching upserts and deletes); status, heartbeat, and error envelopes that
|
|
98
|
+
* already speak the SDK vocabulary pass through unchanged.
|
|
99
|
+
*/
|
|
100
|
+
export declare function decodeHonuaServerRealtimeEvent<TFeature = unknown>(payload: unknown): RealtimeFeatureEvent<TFeature>;
|
|
101
|
+
/**
|
|
102
|
+
* Reusable hook bundle for honua-server's streaming-features endpoint. Spread
|
|
103
|
+
* this into {@link createRealtimeServerSentEventsTransport} options to opt into
|
|
104
|
+
* honua-server's query-param contract and feature-change decoding.
|
|
105
|
+
*/
|
|
106
|
+
export declare function honuaServerRealtimePreset<TFeature = unknown>(): Pick<RealtimeServerSentEventsTransportOptions<TFeature>, "encodeRequest" | "decodeEvent">;
|
|
107
|
+
export interface CreateHonuaServerRealtimeSubscriptionOptions<TFeature = unknown> extends Omit<RealtimeServerSentEventsTransportOptions<TFeature>, "url" | "encodeRequest" | "decodeEvent"> {
|
|
108
|
+
/**
|
|
109
|
+
* honua-server origin, e.g. `https://honua.example`. The
|
|
110
|
+
* `/api/v1/streaming/features` path is appended automatically. Mutually
|
|
111
|
+
* exclusive with {@link CreateHonuaServerRealtimeSubscriptionOptions.url}.
|
|
112
|
+
*/
|
|
113
|
+
readonly baseUrl?: string;
|
|
114
|
+
/**
|
|
115
|
+
* Full streaming-features URL. Use this when the endpoint lives at a
|
|
116
|
+
* non-default path. Overrides {@link CreateHonuaServerRealtimeSubscriptionOptions.baseUrl}.
|
|
117
|
+
*/
|
|
118
|
+
readonly url?: string;
|
|
119
|
+
/**
|
|
120
|
+
* Override the honua-server feature-change decoder while keeping the
|
|
121
|
+
* `serviceId=` / `layers=` query encoding.
|
|
122
|
+
*/
|
|
123
|
+
readonly decodeEvent?: RealtimeServerSentEventsTransportOptions<TFeature>["decodeEvent"];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Convenience factory that builds a {@link RealtimeFeatureTransport} wired to
|
|
127
|
+
* honua-server's `/api/v1/streaming/features` endpoint with the
|
|
128
|
+
* {@link honuaServerRealtimePreset} hooks applied.
|
|
129
|
+
*/
|
|
130
|
+
export declare function createHonuaServerRealtimeSubscription<TFeature = unknown>(options: CreateHonuaServerRealtimeSubscriptionOptions<TFeature>): RealtimeFeatureTransport<TFeature>;
|
|
131
|
+
//# sourceMappingURL=honua-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"honua-server.d.ts","sourceRoot":"","sources":["../../../src/realtime/honua-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-party realtime preset for honua-server's
|
|
3
|
+
* `/api/v1/streaming/features` Server-Sent Events endpoint.
|
|
4
|
+
*
|
|
5
|
+
* The default SSE transport encoder emits `sourceId=` / `layerId=` query
|
|
6
|
+
* params and assumes events already use the SDK's `RealtimeFeatureEvent`
|
|
7
|
+
* vocabulary. honua-server instead expects `serviceId=` / `layers=` query
|
|
8
|
+
* params and emits its own feature-change envelopes. This module packages the
|
|
9
|
+
* two hooks (`encodeRequest` + `decodeEvent`) the transport already exposes so
|
|
10
|
+
* consumers do not have to re-implement the same adapter for every app.
|
|
11
|
+
*
|
|
12
|
+
* @example Use the preset options with the SSE transport.
|
|
13
|
+
* ```ts
|
|
14
|
+
* import {
|
|
15
|
+
* createRealtimeServerSentEventsTransport,
|
|
16
|
+
* honuaServerRealtimePreset,
|
|
17
|
+
* } from "@honua/sdk-js/realtime";
|
|
18
|
+
*
|
|
19
|
+
* const transport = createRealtimeServerSentEventsTransport({
|
|
20
|
+
* url: "https://honua.example/api/v1/streaming/features",
|
|
21
|
+
* ...honuaServerRealtimePreset(),
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @example Or use the convenience factory.
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { createHonuaServerRealtimeSubscription } from "@honua/sdk-js/realtime";
|
|
28
|
+
*
|
|
29
|
+
* const transport = createHonuaServerRealtimeSubscription({
|
|
30
|
+
* baseUrl: "https://honua.example",
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @module
|
|
35
|
+
*/
|
|
36
|
+
import { trimTrailingSlashes } from "../core/path-utils.js";
|
|
37
|
+
import { HonuaRealtimeResumeError } from "./resumable.js";
|
|
38
|
+
import { createRealtimeServerSentEventsTransport, decodeRealtimeServerSentEvent } from "./sse.js";
|
|
39
|
+
/** Default streaming-features path served by honua-server. */
|
|
40
|
+
export const HONUA_SERVER_STREAMING_FEATURES_PATH = "/api/v1/streaming/features";
|
|
41
|
+
/**
|
|
42
|
+
* Encode a `RealtimeSubscriptionRequest` into honua-server's
|
|
43
|
+
* `/api/v1/streaming/features` query contract: `serviceId=` (not `sourceId=`)
|
|
44
|
+
* and `layers=` (not `layerId=`). The remaining filter/resume params match the
|
|
45
|
+
* default encoder so cursors, watermarks, and where-clauses keep working.
|
|
46
|
+
*/
|
|
47
|
+
export function encodeHonuaServerRealtimeRequest(url, request) {
|
|
48
|
+
url.searchParams.set("serviceId", String(request.sourceId));
|
|
49
|
+
if (request.layerId !== undefined)
|
|
50
|
+
url.searchParams.set("layers", String(request.layerId));
|
|
51
|
+
if (request.requestId)
|
|
52
|
+
url.searchParams.set("requestId", request.requestId);
|
|
53
|
+
if (request.mode)
|
|
54
|
+
url.searchParams.set("mode", request.mode);
|
|
55
|
+
if (request.where)
|
|
56
|
+
url.searchParams.set("where", request.where);
|
|
57
|
+
if (request.fields?.length)
|
|
58
|
+
url.searchParams.set("fields", request.fields.join(","));
|
|
59
|
+
if (request.cursor ?? request.resumeFrom?.cursor) {
|
|
60
|
+
url.searchParams.set("cursor", request.cursor ?? request.resumeFrom?.cursor ?? "");
|
|
61
|
+
}
|
|
62
|
+
if (request.watermark ?? request.resumeFrom?.watermark) {
|
|
63
|
+
url.searchParams.set("watermark", request.watermark ?? request.resumeFrom?.watermark ?? "");
|
|
64
|
+
}
|
|
65
|
+
if (request.timestamp ?? request.resumeFrom?.timestamp) {
|
|
66
|
+
url.searchParams.set("timestamp", request.timestamp ?? request.resumeFrom?.timestamp ?? "");
|
|
67
|
+
}
|
|
68
|
+
if (request.deltaToken ?? request.resumeFrom?.deltaToken) {
|
|
69
|
+
url.searchParams.set("deltaToken", request.deltaToken ?? request.resumeFrom?.deltaToken ?? "");
|
|
70
|
+
}
|
|
71
|
+
if (request.resumeFrom?.sequence !== undefined) {
|
|
72
|
+
url.searchParams.set("sequence", String(request.resumeFrom.sequence));
|
|
73
|
+
}
|
|
74
|
+
if (request.spatialFilter !== undefined)
|
|
75
|
+
url.searchParams.set("spatialFilter", JSON.stringify(request.spatialFilter));
|
|
76
|
+
if (request.metadata !== undefined)
|
|
77
|
+
url.searchParams.set("metadata", JSON.stringify(request.metadata));
|
|
78
|
+
return url;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Decode a honua-server `/api/v1/streaming/features` envelope into the SDK's
|
|
82
|
+
* `RealtimeFeatureEvent`. Feature-change envelopes become `delta` events
|
|
83
|
+
* (batching upserts and deletes); status, heartbeat, and error envelopes that
|
|
84
|
+
* already speak the SDK vocabulary pass through unchanged.
|
|
85
|
+
*/
|
|
86
|
+
export function decodeHonuaServerRealtimeEvent(payload) {
|
|
87
|
+
if (!isRecord(payload)) {
|
|
88
|
+
throw new HonuaRealtimeResumeError("invalid-event", "honua-server streaming payload must be a JSON object.");
|
|
89
|
+
}
|
|
90
|
+
const envelope = payload;
|
|
91
|
+
// A current-server change discriminator always wins over generic `type`.
|
|
92
|
+
// Otherwise a malformed/colliding payload could masquerade as status or
|
|
93
|
+
// error and bypass the stricter operation/after-image validation below.
|
|
94
|
+
const currentServerChange = envelope.operation !== undefined;
|
|
95
|
+
// Snapshot / status / heartbeat / error envelopes already use the SDK
|
|
96
|
+
// vocabulary. Their `cursor`, however, is honua-server's global event-store
|
|
97
|
+
// position: a JSON number on the batched `snapshot` baseline frame, where the
|
|
98
|
+
// SDK's resume gate requires the string form it checkpoints. Normalizing it
|
|
99
|
+
// here — the one place that already owns honua-server's wire dialect — keeps
|
|
100
|
+
// a batched baseline from failing closed as a checkpoint conflict.
|
|
101
|
+
if (!currentServerChange &&
|
|
102
|
+
typeof envelope.type === "string" &&
|
|
103
|
+
envelope.type !== "change" &&
|
|
104
|
+
envelope.type !== "feature-change") {
|
|
105
|
+
const framed = withNormalizedNumericCursor(payload);
|
|
106
|
+
if (envelope.type === "status" && (payload.status === "connected" || payload.status === "subscribed")) {
|
|
107
|
+
return decodeRealtimeServerSentEvent({ ...framed, status: "live" });
|
|
108
|
+
}
|
|
109
|
+
return decodeRealtimeServerSentEvent(framed);
|
|
110
|
+
}
|
|
111
|
+
const serviceId = optionalNonEmptyText(envelope.serviceId, "serviceId");
|
|
112
|
+
optionalNonEmptyText(envelope.sourceId, "sourceId");
|
|
113
|
+
const cursor = normalizeCursor(envelope.cursor);
|
|
114
|
+
const sequence = optionalSequence(envelope.sequence);
|
|
115
|
+
if (currentServerChange && serviceId === undefined) {
|
|
116
|
+
throw new HonuaRealtimeResumeError("invalid-event", "honua-server current feature-change envelope is missing serviceId.");
|
|
117
|
+
}
|
|
118
|
+
const changes = collectChanges(envelope);
|
|
119
|
+
if (changes.length === 0) {
|
|
120
|
+
throw new HonuaRealtimeResumeError("invalid-event", "honua-server feature-change envelope is missing changes.");
|
|
121
|
+
}
|
|
122
|
+
const base = {
|
|
123
|
+
eventId: envelope.eventId,
|
|
124
|
+
sequence,
|
|
125
|
+
cursor,
|
|
126
|
+
watermark: envelope.watermark,
|
|
127
|
+
timestamp: envelope.timestamp,
|
|
128
|
+
deltaToken: envelope.deltaToken,
|
|
129
|
+
};
|
|
130
|
+
const upserts = [];
|
|
131
|
+
const deletes = [];
|
|
132
|
+
for (const change of changes) {
|
|
133
|
+
if (!isRecord(change)) {
|
|
134
|
+
throw new HonuaRealtimeResumeError("invalid-event", "honua-server feature change must be an object.");
|
|
135
|
+
}
|
|
136
|
+
const operation = featureChangeOperation(change.op);
|
|
137
|
+
const featureId = featureIdValue(change.featureId);
|
|
138
|
+
if (operation === "delete") {
|
|
139
|
+
deletes.push({ id: featureId, version: change.version, updatedAt: change.updatedAt });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (change.feature === undefined) {
|
|
143
|
+
throw new HonuaRealtimeResumeError("invalid-event", `honua-server ${operation} change is missing feature payload.`);
|
|
144
|
+
}
|
|
145
|
+
upserts.push({
|
|
146
|
+
id: featureId,
|
|
147
|
+
sourceId: serviceId,
|
|
148
|
+
feature: change.feature,
|
|
149
|
+
version: change.version,
|
|
150
|
+
updatedAt: change.updatedAt,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
type: "delta",
|
|
155
|
+
...base,
|
|
156
|
+
...(upserts.length ? { upserts } : {}),
|
|
157
|
+
...(deletes.length ? { deletes: deletes.map((entry) => ({ ...entry, sourceId: serviceId })) } : {}),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Reusable hook bundle for honua-server's streaming-features endpoint. Spread
|
|
162
|
+
* this into {@link createRealtimeServerSentEventsTransport} options to opt into
|
|
163
|
+
* honua-server's query-param contract and feature-change decoding.
|
|
164
|
+
*/
|
|
165
|
+
export function honuaServerRealtimePreset() {
|
|
166
|
+
return {
|
|
167
|
+
encodeRequest: (url, request) => {
|
|
168
|
+
encodeHonuaServerRealtimeRequest(url, request);
|
|
169
|
+
},
|
|
170
|
+
decodeEvent: (decodeHonuaServerRealtimeEvent),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Convenience factory that builds a {@link RealtimeFeatureTransport} wired to
|
|
175
|
+
* honua-server's `/api/v1/streaming/features` endpoint with the
|
|
176
|
+
* {@link honuaServerRealtimePreset} hooks applied.
|
|
177
|
+
*/
|
|
178
|
+
export function createHonuaServerRealtimeSubscription(options) {
|
|
179
|
+
const { baseUrl, url, decodeEvent, ...rest } = options;
|
|
180
|
+
const resolvedUrl = url ?? resolveStreamingUrl(baseUrl);
|
|
181
|
+
const preset = honuaServerRealtimePreset();
|
|
182
|
+
return createRealtimeServerSentEventsTransport({
|
|
183
|
+
...rest,
|
|
184
|
+
url: resolvedUrl,
|
|
185
|
+
encodeRequest: preset.encodeRequest,
|
|
186
|
+
decodeEvent: decodeEvent ?? preset.decodeEvent,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function resolveStreamingUrl(baseUrl) {
|
|
190
|
+
if (!baseUrl) {
|
|
191
|
+
throw new HonuaRealtimeResumeError("invalid-event", "createHonuaServerRealtimeSubscription requires either `url` or `baseUrl`.");
|
|
192
|
+
}
|
|
193
|
+
// Linear trim (no anchored `\/+$` regex) to avoid polynomial backtracking on
|
|
194
|
+
// adversarial input — mirrors the rest of the SDK's path handling.
|
|
195
|
+
return `${trimTrailingSlashes(baseUrl)}${HONUA_SERVER_STREAMING_FEATURES_PATH}`;
|
|
196
|
+
}
|
|
197
|
+
function collectChanges(envelope) {
|
|
198
|
+
if (envelope.operation !== undefined) {
|
|
199
|
+
const operation = currentServerOperation(envelope.operation);
|
|
200
|
+
const featureId = envelope.featureId === "" || envelope.featureId === undefined ? envelope.objectId : envelope.featureId;
|
|
201
|
+
return [
|
|
202
|
+
{
|
|
203
|
+
op: operation,
|
|
204
|
+
featureId,
|
|
205
|
+
feature: operation === "delete" ? undefined : projectCurrentServerFeature(envelope, featureId),
|
|
206
|
+
version: envelope.version,
|
|
207
|
+
updatedAt: envelope.updatedAt ?? envelope.timestamp,
|
|
208
|
+
},
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
if (Array.isArray(envelope.changes))
|
|
212
|
+
return envelope.changes;
|
|
213
|
+
if (envelope.op !== undefined) {
|
|
214
|
+
return [
|
|
215
|
+
{
|
|
216
|
+
op: envelope.op,
|
|
217
|
+
featureId: envelope.featureId,
|
|
218
|
+
feature: envelope.feature,
|
|
219
|
+
version: envelope.version,
|
|
220
|
+
updatedAt: envelope.updatedAt,
|
|
221
|
+
},
|
|
222
|
+
];
|
|
223
|
+
}
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
function projectCurrentServerFeature(envelope, featureId) {
|
|
227
|
+
if (!Object.hasOwn(envelope, "geometry") || envelope.geometry === undefined) {
|
|
228
|
+
throw new HonuaRealtimeResumeError("invalid-event", "honua-server insert/update change is missing a complete geometry after-image.");
|
|
229
|
+
}
|
|
230
|
+
if (!isAttributeRecord(envelope.attributes)) {
|
|
231
|
+
throw new HonuaRealtimeResumeError("invalid-event", "honua-server insert/update change is missing a complete attributes after-image.");
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
type: "Feature",
|
|
235
|
+
...(featureId === undefined ? {} : { id: featureId }),
|
|
236
|
+
geometry: envelope.geometry,
|
|
237
|
+
properties: envelope.attributes,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Render a numeric `cursor` in its string form, leaving every other payload
|
|
242
|
+
* untouched. A malformed numeric cursor is deliberately left as-is so the
|
|
243
|
+
* resume gate still reports it, rather than being converted into a decode
|
|
244
|
+
* failure that would hide which contract was actually violated.
|
|
245
|
+
*/
|
|
246
|
+
function withNormalizedNumericCursor(payload) {
|
|
247
|
+
const cursor = payload.cursor;
|
|
248
|
+
if (typeof cursor !== "number")
|
|
249
|
+
return payload;
|
|
250
|
+
if (!Number.isSafeInteger(cursor) || cursor < 0)
|
|
251
|
+
return payload;
|
|
252
|
+
return { ...payload, cursor: String(cursor) };
|
|
253
|
+
}
|
|
254
|
+
function normalizeCursor(cursor) {
|
|
255
|
+
if (cursor === undefined)
|
|
256
|
+
return undefined;
|
|
257
|
+
if (typeof cursor === "number") {
|
|
258
|
+
if (Number.isSafeInteger(cursor) && cursor >= 0)
|
|
259
|
+
return String(cursor);
|
|
260
|
+
}
|
|
261
|
+
else if (typeof cursor === "string" && cursor.length > 0) {
|
|
262
|
+
return cursor;
|
|
263
|
+
}
|
|
264
|
+
throw new HonuaRealtimeResumeError("invalid-event", "honua-server feature-change cursor must be a non-empty string or non-negative safe integer.");
|
|
265
|
+
}
|
|
266
|
+
function optionalSequence(sequence) {
|
|
267
|
+
if (sequence === undefined)
|
|
268
|
+
return undefined;
|
|
269
|
+
if (typeof sequence === "number" && Number.isSafeInteger(sequence) && sequence >= 0)
|
|
270
|
+
return sequence;
|
|
271
|
+
throw new HonuaRealtimeResumeError("invalid-event", "honua-server feature-change sequence must be a non-negative safe integer.");
|
|
272
|
+
}
|
|
273
|
+
function optionalNonEmptyText(value, field) {
|
|
274
|
+
if (value === undefined || value === null)
|
|
275
|
+
return undefined;
|
|
276
|
+
if (typeof value === "string" && value.length > 0)
|
|
277
|
+
return value;
|
|
278
|
+
throw new HonuaRealtimeResumeError("invalid-event", `honua-server feature-change ${field} must be a non-empty string.`);
|
|
279
|
+
}
|
|
280
|
+
function featureIdValue(value) {
|
|
281
|
+
if (typeof value === "string" && value.length > 0)
|
|
282
|
+
return value;
|
|
283
|
+
if (typeof value === "number" && Number.isSafeInteger(value))
|
|
284
|
+
return value;
|
|
285
|
+
throw new HonuaRealtimeResumeError("invalid-event", "honua-server feature change featureId must be a non-empty string or safe integer.");
|
|
286
|
+
}
|
|
287
|
+
function featureChangeOperation(value) {
|
|
288
|
+
if (value === "insert" || value === "update" || value === "delete" || value === "snapshot")
|
|
289
|
+
return value;
|
|
290
|
+
throw new HonuaRealtimeResumeError("invalid-event", "honua-server feature change operation must be insert, update, delete, or snapshot.");
|
|
291
|
+
}
|
|
292
|
+
function currentServerOperation(value) {
|
|
293
|
+
if (value === "insert" || value === "update" || value === "delete")
|
|
294
|
+
return value;
|
|
295
|
+
throw new HonuaRealtimeResumeError("invalid-event", "honua-server current feature-change operation must be insert, update, or delete.");
|
|
296
|
+
}
|
|
297
|
+
function isAttributeRecord(value) {
|
|
298
|
+
return isRecord(value) && !Array.isArray(value);
|
|
299
|
+
}
|
|
300
|
+
function isRecord(value) {
|
|
301
|
+
return typeof value === "object" && value !== null;
|
|
302
|
+
}
|