@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,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OData v4 delta-link pull adapter for `@honua/sdk-js/realtime` (issue #558).
|
|
3
|
+
*
|
|
4
|
+
* OData delta links (`@odata.deltaLink`) are a *pull* change feed, not a
|
|
5
|
+
* socket transport: a client requests a scoped delta-link URL and the
|
|
6
|
+
* server returns only what changed since that link was minted, plus a fresh
|
|
7
|
+
* delta link for the next request. This module maps that request/response
|
|
8
|
+
* paging protocol onto the transport-neutral `RealtimeFeatureTransport`
|
|
9
|
+
* surface (`types.ts`, #556) so it composes with `createRealtimeFeatureStore`
|
|
10
|
+
* / `createResumableRealtimeSubscription` exactly like the SSE and WebSocket
|
|
11
|
+
* adapters (`sse.ts`, `websocket.ts`, #557) — with one deliberate
|
|
12
|
+
* difference dictated by REQ-004: this transport never claims to be live.
|
|
13
|
+
* `capabilities.kind` is `"polling"`, it never emits a `"heartbeat"`, and
|
|
14
|
+
* `onPoll` reports an explicit cadence/freshness snapshot on every request
|
|
15
|
+
* cycle so a consumer can render "checked N seconds ago, next check in M"
|
|
16
|
+
* rather than a live badge that misrepresents a poll as a stream.
|
|
17
|
+
*
|
|
18
|
+
* Ownership split from the SSE/WebSocket adapters: those open exactly one
|
|
19
|
+
* connection per `subscribe()` and never reconnect on their own, deferring
|
|
20
|
+
* reconnect entirely to `resumable-transport.ts`. This transport has no
|
|
21
|
+
* connection to reconnect — every cycle is an independent request/response —
|
|
22
|
+
* so it owns its own bounded poll loop, including the on-expiry resnapshot
|
|
23
|
+
* recovery a socket transport cannot perform for itself: an expired or
|
|
24
|
+
* rejected delta link (REQ per the User Workflow, "Expired/invalid delta
|
|
25
|
+
* state requires an explicit resnapshot") is recovered by re-running a full
|
|
26
|
+
* snapshot cycle and emitting a fresh `type: "snapshot", replace: true`
|
|
27
|
+
* event, bounded by `maxConsecutiveResnapshots` so a server that keeps
|
|
28
|
+
* rejecting the token cannot loop forever.
|
|
29
|
+
*
|
|
30
|
+
* Security posture (REQ-002): every `@odata.nextLink` / `@odata.deltaLink`
|
|
31
|
+
* this transport follows, and any caller-supplied `resumeFrom.deltaToken`,
|
|
32
|
+
* must resolve to the exact same origin and collection path as the
|
|
33
|
+
* configured `url`. A link that does not is rejected rather than followed —
|
|
34
|
+
* a delta token is an opaque, potentially attacker-influenced value once it
|
|
35
|
+
* has round-tripped through a checkpoint store, and this transport never
|
|
36
|
+
* trusts it to redirect a request elsewhere.
|
|
37
|
+
*
|
|
38
|
+
* @module
|
|
39
|
+
*/
|
|
40
|
+
import { HonuaRealtimeResumeError } from "./resumable.js";
|
|
41
|
+
import { realtimeFailure } from "./wire.js";
|
|
42
|
+
const DEFAULT_MAX_PAGES_PER_CYCLE = 500;
|
|
43
|
+
const DEFAULT_MAX_SNAPSHOT_ROWS = 50_000;
|
|
44
|
+
const DEFAULT_MAX_CONSECUTIVE_RESNAPSHOTS = 3;
|
|
45
|
+
/**
|
|
46
|
+
* Build a {@link RealtimeFeatureTransport} that polls an OData v4 entity
|
|
47
|
+
* set's delta link. See the module doc for the full recovery/security model.
|
|
48
|
+
*/
|
|
49
|
+
export function createOdataDeltaTransport(options) {
|
|
50
|
+
if (typeof options.entityId !== "function") {
|
|
51
|
+
throw new TypeError("createOdataDeltaTransport requires options.entityId.");
|
|
52
|
+
}
|
|
53
|
+
const pollIntervalMs = positiveInteger(options.pollIntervalMs, "pollIntervalMs");
|
|
54
|
+
const maxPagesPerCycle = positiveInteger(options.maxPagesPerCycle ?? DEFAULT_MAX_PAGES_PER_CYCLE, "maxPagesPerCycle");
|
|
55
|
+
const maxSnapshotRows = positiveInteger(options.maxSnapshotRows ?? DEFAULT_MAX_SNAPSHOT_ROWS, "maxSnapshotRows");
|
|
56
|
+
const maxConsecutiveResnapshots = positiveInteger(options.maxConsecutiveResnapshots ?? DEFAULT_MAX_CONSECUTIVE_RESNAPSHOTS, "maxConsecutiveResnapshots");
|
|
57
|
+
const collectionUrl = parseAbsoluteUrl(options.url, `createOdataDeltaTransport requires an absolute options.url ("${options.url}").`);
|
|
58
|
+
return {
|
|
59
|
+
capabilities: {
|
|
60
|
+
kind: "polling",
|
|
61
|
+
resumeModes: ["delta-token"],
|
|
62
|
+
emitsHeartbeats: false,
|
|
63
|
+
emitsWatermarks: false,
|
|
64
|
+
},
|
|
65
|
+
subscribe(request, observer) {
|
|
66
|
+
if (request.signal?.aborted) {
|
|
67
|
+
observer.complete();
|
|
68
|
+
return { close: () => { } };
|
|
69
|
+
}
|
|
70
|
+
const resumeDeltaToken = request.deltaToken ?? request.resumeFrom?.deltaToken;
|
|
71
|
+
let initialDeltaLink;
|
|
72
|
+
if (resumeDeltaToken !== undefined) {
|
|
73
|
+
const resumeUrl = parseRelativeOdataUrl(resumeDeltaToken, collectionUrl, `Realtime resumeFrom.deltaToken "${resumeDeltaToken}" is not a valid URL.`);
|
|
74
|
+
assertSameOdataCollection(collectionUrl, resumeUrl);
|
|
75
|
+
initialDeltaLink = resumeUrl.toString();
|
|
76
|
+
}
|
|
77
|
+
const now = () => options.now?.() ?? Date.now();
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
let closed = false;
|
|
80
|
+
let sequence = request.resumeFrom?.sequence ?? 0;
|
|
81
|
+
let resnapshotStreak = 0;
|
|
82
|
+
let timer;
|
|
83
|
+
const abortListener = () => teardown("Subscription signal was aborted.", true);
|
|
84
|
+
request.signal?.addEventListener("abort", abortListener, { once: true });
|
|
85
|
+
function teardown(reason, complete) {
|
|
86
|
+
if (closed)
|
|
87
|
+
return;
|
|
88
|
+
closed = true;
|
|
89
|
+
if (timer !== undefined)
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
request.signal?.removeEventListener("abort", abortListener);
|
|
92
|
+
if (!controller.signal.aborted)
|
|
93
|
+
controller.abort(reason);
|
|
94
|
+
if (complete)
|
|
95
|
+
observer.complete();
|
|
96
|
+
}
|
|
97
|
+
function fail(error) {
|
|
98
|
+
if (closed)
|
|
99
|
+
return;
|
|
100
|
+
closed = true;
|
|
101
|
+
if (timer !== undefined)
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
request.signal?.removeEventListener("abort", abortListener);
|
|
104
|
+
if (!controller.signal.aborted)
|
|
105
|
+
controller.abort("OData delta transport failed.");
|
|
106
|
+
observer.error(error);
|
|
107
|
+
}
|
|
108
|
+
function emit(event) {
|
|
109
|
+
if (closed)
|
|
110
|
+
return false;
|
|
111
|
+
try {
|
|
112
|
+
observer.next(event);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
catch (cause) {
|
|
116
|
+
fail(realtimeFailure("consumer-failed", "Realtime event observer rejected delivery.", cause));
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function reportPoll(partial) {
|
|
121
|
+
if (!options.onPoll)
|
|
122
|
+
return;
|
|
123
|
+
try {
|
|
124
|
+
options.onPoll({
|
|
125
|
+
polledAt: now(),
|
|
126
|
+
intervalMs: pollIntervalMs,
|
|
127
|
+
sequence,
|
|
128
|
+
changed: partial.changed,
|
|
129
|
+
upsertCount: partial.upsertCount,
|
|
130
|
+
deleteCount: partial.deleteCount,
|
|
131
|
+
...(partial.scheduled ? { nextPollAt: now() + pollIntervalMs } : {}),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Telemetry is diagnostic only; a throwing onPoll callback must
|
|
136
|
+
// never affect delivery.
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function schedulePoll(link) {
|
|
140
|
+
if (closed)
|
|
141
|
+
return;
|
|
142
|
+
timer = setTimeout(() => {
|
|
143
|
+
timer = undefined;
|
|
144
|
+
void runDeltaCycle(link);
|
|
145
|
+
}, pollIntervalMs);
|
|
146
|
+
}
|
|
147
|
+
async function fetchOdataDocument(target, trackChanges) {
|
|
148
|
+
const fetchImpl = resolveFetch(options.fetchImpl);
|
|
149
|
+
const headers = { Accept: "application/json", ...(options.headers?.() ?? {}) };
|
|
150
|
+
if (trackChanges)
|
|
151
|
+
headers.Prefer = "odata.track-changes";
|
|
152
|
+
let response;
|
|
153
|
+
try {
|
|
154
|
+
response = await fetchImpl(target, { method: "GET", headers, signal: controller.signal });
|
|
155
|
+
}
|
|
156
|
+
catch (cause) {
|
|
157
|
+
if (controller.signal.aborted)
|
|
158
|
+
throw cause;
|
|
159
|
+
throw realtimeFailure("transport-gap", "OData delta request failed.", cause);
|
|
160
|
+
}
|
|
161
|
+
if (!response.ok) {
|
|
162
|
+
const body = await safeReadJson(response);
|
|
163
|
+
const isExpired = (options.isDeltaLinkExpiredResponse ?? defaultIsDeltaLinkExpiredResponse)(response.status, body);
|
|
164
|
+
if (isExpired) {
|
|
165
|
+
throw new HonuaRealtimeResumeError("cursor-expired", `OData delta link expired or was rejected by the server (HTTP ${String(response.status)}).`);
|
|
166
|
+
}
|
|
167
|
+
throw realtimeFailure(response.status >= 500 ? "transport-gap" : "invalid-event", `OData delta request failed with HTTP ${String(response.status)}${response.statusText ? ` ${response.statusText}` : ""}.`, body);
|
|
168
|
+
}
|
|
169
|
+
const body = await safeReadJson(response);
|
|
170
|
+
if (!isRecord(body) || !Array.isArray(body.value)) {
|
|
171
|
+
throw realtimeFailure("invalid-event", "OData delta response is missing a JSON `value` array.", body);
|
|
172
|
+
}
|
|
173
|
+
return body;
|
|
174
|
+
}
|
|
175
|
+
function parseEntry(raw) {
|
|
176
|
+
if (!isRecord(raw)) {
|
|
177
|
+
throw realtimeFailure("invalid-event", "OData delta entry must be a JSON object.", raw);
|
|
178
|
+
}
|
|
179
|
+
const removed = raw["@removed"] ?? raw["@odata.removed"];
|
|
180
|
+
if (removed !== undefined) {
|
|
181
|
+
return { kind: "delete", patch: { id: removedEntryId(raw) } };
|
|
182
|
+
}
|
|
183
|
+
const isRelationshipDelta = options.isRelationshipDeltaEntry ?? defaultIsRelationshipDeltaEntry;
|
|
184
|
+
if (isRelationshipDelta(raw)) {
|
|
185
|
+
throw realtimeFailure("invalid-event", "OData relationship (link) deltas are not supported by createOdataDeltaTransport; only entity upserts and removals are normalized (REQ-003).", raw);
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const id = options.entityId(raw);
|
|
189
|
+
const feature = (options.toFeature ?? defaultToFeature)(raw);
|
|
190
|
+
return { kind: "upsert", patch: { id, feature } };
|
|
191
|
+
}
|
|
192
|
+
catch (cause) {
|
|
193
|
+
throw realtimeFailure("invalid-event", "OData delta entry projection (entityId/toFeature) failed.", cause);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function removedEntryId(entry) {
|
|
197
|
+
const withoutAnnotations = Object.fromEntries(Object.entries(entry).filter(([key]) => !key.startsWith("@")));
|
|
198
|
+
if (Object.keys(withoutAnnotations).length > 0) {
|
|
199
|
+
try {
|
|
200
|
+
return options.entityId(withoutAnnotations);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Fall through to @id parsing below.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const idValue = entry["@id"] ?? entry["@odata.id"];
|
|
207
|
+
if (typeof idValue !== "string") {
|
|
208
|
+
throw realtimeFailure("invalid-event", "OData removed delta entry has neither key properties nor an @id/@odata.id to derive an id from.", entry);
|
|
209
|
+
}
|
|
210
|
+
const key = parseODataIdKey(idValue);
|
|
211
|
+
if (key === undefined) {
|
|
212
|
+
throw realtimeFailure("invalid-event", `OData removed delta entry's id "${idValue}" uses a composite or unparseable key; the removed entry must retain key properties instead.`, entry);
|
|
213
|
+
}
|
|
214
|
+
return key;
|
|
215
|
+
}
|
|
216
|
+
async function runCycle(startUrl, mode) {
|
|
217
|
+
let target = startUrl;
|
|
218
|
+
let pages = 0;
|
|
219
|
+
const upserts = [];
|
|
220
|
+
const deletes = [];
|
|
221
|
+
let deltaLink;
|
|
222
|
+
while (true) {
|
|
223
|
+
pages += 1;
|
|
224
|
+
if (pages > maxPagesPerCycle) {
|
|
225
|
+
throw realtimeFailure("delivery-failed", `OData delta paging exceeded the configured ${String(maxPagesPerCycle)}-page bound.`, undefined);
|
|
226
|
+
}
|
|
227
|
+
const envelope = await fetchOdataDocument(target, mode === "snapshot" && pages === 1);
|
|
228
|
+
for (const raw of envelope.value) {
|
|
229
|
+
const parsed = parseEntry(raw);
|
|
230
|
+
if (parsed.kind === "delete")
|
|
231
|
+
deletes.push(parsed.patch);
|
|
232
|
+
else
|
|
233
|
+
upserts.push(parsed.patch);
|
|
234
|
+
if (mode === "snapshot" && upserts.length > maxSnapshotRows) {
|
|
235
|
+
throw realtimeFailure("delivery-failed", `OData snapshot exceeded the configured ${String(maxSnapshotRows)}-row bound.`, undefined);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const pageDeltaLink = envelope["@odata.deltaLink"];
|
|
239
|
+
if (pageDeltaLink !== undefined) {
|
|
240
|
+
const resolved = parseRelativeOdataUrl(pageDeltaLink, target, `OData response @odata.deltaLink "${pageDeltaLink}" is not a valid URL.`);
|
|
241
|
+
assertSameOdataCollection(collectionUrl, resolved);
|
|
242
|
+
deltaLink = resolved.toString();
|
|
243
|
+
}
|
|
244
|
+
const nextLink = envelope["@odata.nextLink"];
|
|
245
|
+
if (nextLink === undefined)
|
|
246
|
+
break;
|
|
247
|
+
const resolvedNext = parseRelativeOdataUrl(nextLink, target, `OData response @odata.nextLink "${nextLink}" is not a valid URL.`);
|
|
248
|
+
assertSameOdataCollection(collectionUrl, resolvedNext);
|
|
249
|
+
target = resolvedNext.toString();
|
|
250
|
+
}
|
|
251
|
+
if (deltaLink === undefined) {
|
|
252
|
+
throw realtimeFailure("invalid-event", "OData response did not include a terminal @odata.deltaLink; the server may not support delta-link change tracking for this collection.", undefined);
|
|
253
|
+
}
|
|
254
|
+
return { upserts, deletes, deltaLink };
|
|
255
|
+
}
|
|
256
|
+
function buildInitialUrl() {
|
|
257
|
+
const url = new URL(collectionUrl.toString());
|
|
258
|
+
const query = options.initialQuery;
|
|
259
|
+
if (query?.filter)
|
|
260
|
+
url.searchParams.set("$filter", query.filter);
|
|
261
|
+
if (query?.select?.length)
|
|
262
|
+
url.searchParams.set("$select", query.select.join(","));
|
|
263
|
+
if (query?.orderBy?.length)
|
|
264
|
+
url.searchParams.set("$orderby", query.orderBy.join(","));
|
|
265
|
+
if (typeof query?.top === "number" && Number.isFinite(query.top)) {
|
|
266
|
+
url.searchParams.set("$top", String(query.top));
|
|
267
|
+
}
|
|
268
|
+
return url.toString();
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Shared recovery path for a `cursor-expired` failure from *either*
|
|
272
|
+
* cycle: a real server should only ever reject a `$deltatoken`, but a
|
|
273
|
+
* caller-supplied `isDeltaLinkExpiredResponse` could in principle
|
|
274
|
+
* classify any response that way, so both cycles route through the
|
|
275
|
+
* same bounded streak counter (REQ-005) rather than the snapshot
|
|
276
|
+
* cycle failing closed on its very first retry.
|
|
277
|
+
*/
|
|
278
|
+
async function recoverFromExpiredDeltaLink(cause) {
|
|
279
|
+
resnapshotStreak += 1;
|
|
280
|
+
if (resnapshotStreak > maxConsecutiveResnapshots) {
|
|
281
|
+
fail(realtimeFailure("delivery-failed", `OData delta link expired ${String(resnapshotStreak)} times consecutively; giving up after the configured ${String(maxConsecutiveResnapshots)}-resnapshot bound.`, cause));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
reportPoll({ changed: false, upsertCount: 0, deleteCount: 0, scheduled: false });
|
|
285
|
+
const delivered = emit({
|
|
286
|
+
type: "status",
|
|
287
|
+
status: "reconnecting",
|
|
288
|
+
reason: "cursor-expired",
|
|
289
|
+
receivedAt: now(),
|
|
290
|
+
});
|
|
291
|
+
if (!delivered)
|
|
292
|
+
return;
|
|
293
|
+
await runSnapshotCycle();
|
|
294
|
+
}
|
|
295
|
+
async function runSnapshotCycle() {
|
|
296
|
+
try {
|
|
297
|
+
const cycle = await runCycle(buildInitialUrl(), "snapshot");
|
|
298
|
+
if (closed)
|
|
299
|
+
return;
|
|
300
|
+
resnapshotStreak = 0;
|
|
301
|
+
sequence += 1;
|
|
302
|
+
const delivered = emit({
|
|
303
|
+
type: "snapshot",
|
|
304
|
+
sequence,
|
|
305
|
+
deltaToken: cycle.deltaLink,
|
|
306
|
+
receivedAt: now(),
|
|
307
|
+
replace: true,
|
|
308
|
+
features: cycle.upserts,
|
|
309
|
+
});
|
|
310
|
+
if (!delivered)
|
|
311
|
+
return;
|
|
312
|
+
reportPoll({
|
|
313
|
+
changed: true,
|
|
314
|
+
upsertCount: cycle.upserts.length,
|
|
315
|
+
deleteCount: 0,
|
|
316
|
+
scheduled: true,
|
|
317
|
+
});
|
|
318
|
+
schedulePoll(cycle.deltaLink);
|
|
319
|
+
}
|
|
320
|
+
catch (cause) {
|
|
321
|
+
if (closed)
|
|
322
|
+
return;
|
|
323
|
+
if (cause instanceof HonuaRealtimeResumeError && cause.code === "cursor-expired") {
|
|
324
|
+
await recoverFromExpiredDeltaLink(cause);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
fail(cause);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async function runDeltaCycle(link) {
|
|
331
|
+
try {
|
|
332
|
+
const cycle = await runCycle(link, "delta");
|
|
333
|
+
if (closed)
|
|
334
|
+
return;
|
|
335
|
+
resnapshotStreak = 0;
|
|
336
|
+
const changed = cycle.upserts.length > 0 || cycle.deletes.length > 0;
|
|
337
|
+
if (changed)
|
|
338
|
+
sequence += 1;
|
|
339
|
+
const delivered = emit(changed
|
|
340
|
+
? {
|
|
341
|
+
type: "delta",
|
|
342
|
+
sequence,
|
|
343
|
+
deltaToken: cycle.deltaLink,
|
|
344
|
+
receivedAt: now(),
|
|
345
|
+
...(cycle.upserts.length ? { upserts: cycle.upserts } : {}),
|
|
346
|
+
...(cycle.deletes.length ? { deletes: cycle.deletes } : {}),
|
|
347
|
+
}
|
|
348
|
+
: {
|
|
349
|
+
type: "status",
|
|
350
|
+
status: "live",
|
|
351
|
+
sequence,
|
|
352
|
+
deltaToken: cycle.deltaLink,
|
|
353
|
+
receivedAt: now(),
|
|
354
|
+
reason: "poll-unchanged",
|
|
355
|
+
});
|
|
356
|
+
if (!delivered)
|
|
357
|
+
return;
|
|
358
|
+
reportPoll({
|
|
359
|
+
changed,
|
|
360
|
+
upsertCount: cycle.upserts.length,
|
|
361
|
+
deleteCount: cycle.deletes.length,
|
|
362
|
+
scheduled: true,
|
|
363
|
+
});
|
|
364
|
+
schedulePoll(cycle.deltaLink);
|
|
365
|
+
}
|
|
366
|
+
catch (cause) {
|
|
367
|
+
if (closed)
|
|
368
|
+
return;
|
|
369
|
+
if (cause instanceof HonuaRealtimeResumeError && cause.code === "cursor-expired") {
|
|
370
|
+
await recoverFromExpiredDeltaLink(cause);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
fail(cause);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
void (initialDeltaLink !== undefined ? runDeltaCycle(initialDeltaLink) : runSnapshotCycle());
|
|
377
|
+
return {
|
|
378
|
+
close() {
|
|
379
|
+
teardown("Subscription closed by caller.", true);
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function defaultIsDeltaLinkExpiredResponse(status) {
|
|
386
|
+
return status === 410;
|
|
387
|
+
}
|
|
388
|
+
function defaultIsRelationshipDeltaEntry(entry) {
|
|
389
|
+
return typeof entry.source === "string" && typeof entry.relationship === "string" && typeof entry.target === "string";
|
|
390
|
+
}
|
|
391
|
+
function defaultToFeature(entity) {
|
|
392
|
+
const clean = {};
|
|
393
|
+
for (const [key, value] of Object.entries(entity)) {
|
|
394
|
+
if (!key.startsWith("@"))
|
|
395
|
+
clean[key] = value;
|
|
396
|
+
}
|
|
397
|
+
return clean;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Reject a next/delta link that does not resolve to the exact origin and
|
|
401
|
+
* collection path this transport was configured for (REQ-002). Trailing
|
|
402
|
+
* slashes are normalized so `.../Incidents` and `.../Incidents/` are treated
|
|
403
|
+
* as the same collection.
|
|
404
|
+
*/
|
|
405
|
+
function assertSameOdataCollection(expected, candidate) {
|
|
406
|
+
if (candidate.origin !== expected.origin || normalizedPathname(candidate) !== normalizedPathname(expected)) {
|
|
407
|
+
throw new HonuaRealtimeResumeError("invalid-event", `OData delta/next link "${candidate.toString()}" does not resolve to the configured collection "${expected.toString()}"; refusing to follow a foreign link.`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function normalizedPathname(url) {
|
|
411
|
+
return url.pathname.length > 1 && url.pathname.endsWith("/") ? url.pathname.slice(0, -1) : url.pathname;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Parse the single-value key predicate out of an OData `@id`/`@odata.id`
|
|
415
|
+
* value like `Incidents('AB-12')` or `Incidents(42)`. Returns `undefined`
|
|
416
|
+
* for composite keys (`Key1=1,Key2='a'`) or an unparseable shape — callers
|
|
417
|
+
* treat that as unsupported rather than guessing.
|
|
418
|
+
*/
|
|
419
|
+
function parseODataIdKey(idValue) {
|
|
420
|
+
const open = idValue.lastIndexOf("(");
|
|
421
|
+
if (open === -1 || !idValue.endsWith(")"))
|
|
422
|
+
return undefined;
|
|
423
|
+
const inner = idValue.slice(open + 1, -1);
|
|
424
|
+
if (inner.includes("=") || hasTopLevelComma(inner))
|
|
425
|
+
return undefined;
|
|
426
|
+
const unquoted = inner.startsWith("'") && inner.endsWith("'") ? inner.slice(1, -1).replace(/''/g, "'") : inner;
|
|
427
|
+
if (unquoted.length === 0)
|
|
428
|
+
return undefined;
|
|
429
|
+
if (/^-?\d+$/.test(unquoted) && Number.isSafeInteger(Number(unquoted)))
|
|
430
|
+
return Number(unquoted);
|
|
431
|
+
return unquoted;
|
|
432
|
+
}
|
|
433
|
+
function hasTopLevelComma(inner) {
|
|
434
|
+
let quoted = false;
|
|
435
|
+
for (const character of inner) {
|
|
436
|
+
if (character === "'")
|
|
437
|
+
quoted = !quoted;
|
|
438
|
+
else if (character === "," && !quoted)
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
function parseAbsoluteUrl(input, message) {
|
|
444
|
+
try {
|
|
445
|
+
return new URL(input);
|
|
446
|
+
}
|
|
447
|
+
catch (cause) {
|
|
448
|
+
throw new TypeError(message, { cause });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function parseRelativeOdataUrl(value, base, message) {
|
|
452
|
+
try {
|
|
453
|
+
return new URL(value, base);
|
|
454
|
+
}
|
|
455
|
+
catch (cause) {
|
|
456
|
+
throw new HonuaRealtimeResumeError("invalid-event", message, { cause });
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
function resolveFetch(custom) {
|
|
460
|
+
if (custom)
|
|
461
|
+
return custom;
|
|
462
|
+
const globalFetch = globalThis.fetch;
|
|
463
|
+
if (!globalFetch)
|
|
464
|
+
throw new Error("fetch is not available; provide fetchImpl for this runtime.");
|
|
465
|
+
return globalFetch.bind(globalThis);
|
|
466
|
+
}
|
|
467
|
+
async function safeReadJson(response) {
|
|
468
|
+
try {
|
|
469
|
+
return await response.json();
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function isRecord(value) {
|
|
476
|
+
return typeof value === "object" && value !== null;
|
|
477
|
+
}
|
|
478
|
+
function positiveInteger(value, name) {
|
|
479
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
480
|
+
throw new TypeError(`createOdataDeltaTransport: ${name} must be a safe integer greater than zero.`);
|
|
481
|
+
}
|
|
482
|
+
return value;
|
|
483
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projection helpers for map, table, and detail views that consume realtime
|
|
3
|
+
* feature state.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import type { FeatureId, SourceId } from "../contract/types.js";
|
|
8
|
+
import type { RealtimeFeatureRecord, RealtimeFeatureState, RealtimeFeatureTombstone } from "./types.js";
|
|
9
|
+
export interface RealtimeFeatureRecordSelectorOptions<TFeature = unknown> {
|
|
10
|
+
readonly sourceId?: SourceId;
|
|
11
|
+
readonly sort?: (left: RealtimeFeatureRecord<TFeature>, right: RealtimeFeatureRecord<TFeature>) => number;
|
|
12
|
+
}
|
|
13
|
+
export interface RealtimeFeatureReference {
|
|
14
|
+
readonly id: FeatureId;
|
|
15
|
+
readonly sourceId?: SourceId;
|
|
16
|
+
}
|
|
17
|
+
export interface RealtimeDetailSelectorOptions {
|
|
18
|
+
readonly sourceId?: SourceId;
|
|
19
|
+
}
|
|
20
|
+
export interface RealtimeDetailState<TFeature = unknown> {
|
|
21
|
+
readonly status: "present" | "deleted" | "missing";
|
|
22
|
+
readonly record?: RealtimeFeatureRecord<TFeature>;
|
|
23
|
+
readonly tombstone?: RealtimeFeatureTombstone;
|
|
24
|
+
}
|
|
25
|
+
export declare function selectRealtimeFeatureRecords<TFeature>(state: RealtimeFeatureState<TFeature>, options?: RealtimeFeatureRecordSelectorOptions<TFeature>): RealtimeFeatureRecord<TFeature>[];
|
|
26
|
+
export declare function selectRealtimeFeatures<TFeature>(state: RealtimeFeatureState<TFeature>, options?: RealtimeFeatureRecordSelectorOptions<TFeature>): TFeature[];
|
|
27
|
+
export declare function selectRealtimeFeatureRecordMap<TFeature>(state: RealtimeFeatureState<TFeature>, options?: RealtimeFeatureRecordSelectorOptions<TFeature>): Readonly<Record<string, RealtimeFeatureRecord<TFeature>>>;
|
|
28
|
+
export declare function selectRealtimeFeatureTombstones(state: RealtimeFeatureState<unknown>, options?: RealtimeDetailSelectorOptions): RealtimeFeatureTombstone[];
|
|
29
|
+
export declare function selectRealtimeDetail<TFeature>(state: RealtimeFeatureState<TFeature>, target: FeatureId | RealtimeFeatureReference, options?: RealtimeDetailSelectorOptions): RealtimeDetailState<TFeature>;
|
|
30
|
+
//# sourceMappingURL=projections.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projections.d.ts","sourceRoot":"","sources":["../../../src/realtime/projections.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAEhE,OAAO,KAAK,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAExG,MAAM,WAAW,oCAAoC,CAAC,QAAQ,GAAG,OAAO;IACtE,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,qBAAqB,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,qBAAqB,CAAC,QAAQ,CAAC,KAAK,MAAM,CAAC;CAC3G;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;IACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;CAC9B;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB,CAAC,QAAQ,GAAG,OAAO;IACrD,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;IACnD,QAAQ,CAAC,MAAM,CAAC,EAAE,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IAClD,QAAQ,CAAC,SAAS,CAAC,EAAE,wBAAwB,CAAC;CAC/C;AAED,wBAAgB,4BAA4B,CAAC,QAAQ,EACnD,KAAK,EAAE,oBAAoB,CAAC,QAAQ,CAAC,EACrC,OAAO,GAAE,oCAAoC,CAAC,QAAQ,CAAM,GAC3D,qBAAqB,CAAC,QAAQ,CAAC,EAAE,CAGnC;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAC7C,KAAK,EAAE,oBAAoB,CAAC,QAAQ,CAAC,EACrC,OAAO,GAAE,oCAAoC,CAAC,QAAQ,CAAM,GAC3D,QAAQ,EAAE,CAEZ;AAED,wBAAgB,8BAA8B,CAAC,QAAQ,EACrD,KAAK,EAAE,oBAAoB,CAAC,QAAQ,CAAC,EACrC,OAAO,GAAE,oCAAoC,CAAC,QAAQ,CAAM,GAC3D,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAM3D;AAED,wBAAgB,+BAA+B,CAC7C,KAAK,EAAE,oBAAoB,CAAC,OAAO,CAAC,EACpC,OAAO,GAAE,6BAAkC,GAC1C,wBAAwB,EAAE,CAE5B;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAC3C,KAAK,EAAE,oBAAoB,CAAC,QAAQ,CAAC,EACrC,MAAM,EAAE,SAAS,GAAG,wBAAwB,EAC5C,OAAO,GAAE,6BAAkC,GAC1C,mBAAmB,CAAC,QAAQ,CAAC,CAiB/B"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projection helpers for map, table, and detail views that consume realtime
|
|
3
|
+
* feature state.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { realtimeFeatureKey } from "./reducer.js";
|
|
8
|
+
export function selectRealtimeFeatureRecords(state, options = {}) {
|
|
9
|
+
const records = Object.values(state.records).filter((record) => matchesSource(record, options.sourceId));
|
|
10
|
+
return options.sort ? [...records].sort(options.sort) : records;
|
|
11
|
+
}
|
|
12
|
+
export function selectRealtimeFeatures(state, options = {}) {
|
|
13
|
+
return selectRealtimeFeatureRecords(state, options).map((record) => record.feature);
|
|
14
|
+
}
|
|
15
|
+
export function selectRealtimeFeatureRecordMap(state, options = {}) {
|
|
16
|
+
const records = {};
|
|
17
|
+
for (const record of selectRealtimeFeatureRecords(state, options)) {
|
|
18
|
+
records[record.key] = record;
|
|
19
|
+
}
|
|
20
|
+
return records;
|
|
21
|
+
}
|
|
22
|
+
export function selectRealtimeFeatureTombstones(state, options = {}) {
|
|
23
|
+
return Object.values(state.tombstones).filter((tombstone) => matchesSource(tombstone, options.sourceId));
|
|
24
|
+
}
|
|
25
|
+
export function selectRealtimeDetail(state, target, options = {}) {
|
|
26
|
+
const key = realtimeReferenceKey(target, options.sourceId);
|
|
27
|
+
const record = state.records[key];
|
|
28
|
+
if (record) {
|
|
29
|
+
return {
|
|
30
|
+
status: "present",
|
|
31
|
+
record,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const tombstone = state.tombstones[key];
|
|
35
|
+
if (tombstone) {
|
|
36
|
+
return {
|
|
37
|
+
status: "deleted",
|
|
38
|
+
tombstone,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { status: "missing" };
|
|
42
|
+
}
|
|
43
|
+
function matchesSource(record, sourceId) {
|
|
44
|
+
return sourceId === undefined || record.sourceId === sourceId;
|
|
45
|
+
}
|
|
46
|
+
function realtimeReferenceKey(target, sourceId) {
|
|
47
|
+
if (typeof target === "object" && target !== null) {
|
|
48
|
+
return realtimeFeatureKey(target.sourceId ?? sourceId, target.id);
|
|
49
|
+
}
|
|
50
|
+
return realtimeFeatureKey(sourceId, target);
|
|
51
|
+
}
|