@honua/react 0.1.2-beta.0 → 0.1.7-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 +88 -19
- package/contract/types.d.ts.map +1 -1
- package/contract/types.js +4 -0
- package/contract/widget-source.d.ts.map +1 -1
- package/core/client.d.ts +51 -21
- package/core/client.d.ts.map +1 -1
- package/core/client.js +234 -54
- package/core/error-classifications.d.ts +23 -0
- package/core/error-classifications.d.ts.map +1 -1
- package/core/error-classifications.js +23 -0
- package/core/error-code-registry.d.ts +24 -1
- package/core/error-code-registry.d.ts.map +1 -1
- package/core/error-code-registry.js +23 -0
- package/core/error-envelope.js +2 -0
- package/core/errors.d.ts +2 -2
- package/core/errors.d.ts.map +1 -1
- package/core/errors.js +1 -1
- package/core/geoparquet-sql.d.ts +69 -5
- package/core/geoparquet-sql.d.ts.map +1 -1
- package/core/geoparquet-sql.js +327 -10
- 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 +32 -0
- package/core/stac.d.ts.map +1 -1
- package/core/stac.js +158 -23
- package/core/surfaces.d.ts.map +1 -1
- package/core/surfaces.js +273 -68
- package/core/types.d.ts +138 -11
- 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/interactions/declarative-contract.d.ts +149 -0
- package/interactions/declarative-contract.d.ts.map +1 -0
- package/interactions/declarative-contract.js +293 -0
- package/interactions/declarative.d.ts +198 -0
- package/interactions/declarative.d.ts.map +1 -0
- package/interactions/declarative.js +470 -0
- package/interactions/feature-state.d.ts +7 -0
- package/interactions/feature-state.d.ts.map +1 -1
- package/interactions/feature-state.js +10 -2
- 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/maplibre-target.js +1 -1
- 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 +153 -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/runtime/style-compose.d.ts.map +1 -1
- package/runtime/style-compose.js +78 -9
- 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,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure reducer helpers for realtime feature state.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
const DEFAULT_MAX_SEEN_EVENT_IDS = 256;
|
|
7
|
+
const DEFAULT_MAX_TOMBSTONES = 1024;
|
|
8
|
+
export function emptyRealtimeFeatureState() {
|
|
9
|
+
return {
|
|
10
|
+
status: "idle",
|
|
11
|
+
records: {},
|
|
12
|
+
tombstones: {},
|
|
13
|
+
ignoredEventCount: 0,
|
|
14
|
+
seenEventIds: [],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function realtimeFeatureKey(sourceId, id) {
|
|
18
|
+
return `${sourceId ?? ""}:${String(id)}`;
|
|
19
|
+
}
|
|
20
|
+
export function realtimeSubscriptionKey(request) {
|
|
21
|
+
return [
|
|
22
|
+
request.requestId ?? "",
|
|
23
|
+
request.sourceId,
|
|
24
|
+
request.layerId === undefined ? "" : String(request.layerId),
|
|
25
|
+
request.where ?? "",
|
|
26
|
+
request.fields?.join(",") ?? "",
|
|
27
|
+
encodeIdentityPart(request.spatialFilter),
|
|
28
|
+
].join("\u0000");
|
|
29
|
+
}
|
|
30
|
+
export function realtimeResumeCheckpoint(state) {
|
|
31
|
+
if (!state.checkpoint)
|
|
32
|
+
return undefined;
|
|
33
|
+
return { ...state.checkpoint };
|
|
34
|
+
}
|
|
35
|
+
export function reduceRealtimeFeatureState(state, event, options = {}) {
|
|
36
|
+
if (isDuplicateOrOutOfOrder(state, event)) {
|
|
37
|
+
return {
|
|
38
|
+
...state,
|
|
39
|
+
ignoredEventCount: state.ignoredEventCount + 1,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const receivedAt = event.receivedAt ?? options.now?.() ?? Date.now();
|
|
43
|
+
const base = withEventMetadata(state, event, receivedAt, options.maxSeenEventIds ?? DEFAULT_MAX_SEEN_EVENT_IDS);
|
|
44
|
+
const metadata = mutationMetadata(event, receivedAt);
|
|
45
|
+
switch (event.type) {
|
|
46
|
+
case "snapshot":
|
|
47
|
+
return applySnapshot(base, event.features, {
|
|
48
|
+
replace: event.replace ?? true,
|
|
49
|
+
...metadata,
|
|
50
|
+
});
|
|
51
|
+
case "upsert":
|
|
52
|
+
return applyUpsert(base, event.feature, metadata);
|
|
53
|
+
case "delete":
|
|
54
|
+
return boundTombstones(applyDelete(base, {
|
|
55
|
+
id: event.id,
|
|
56
|
+
sourceId: event.sourceId,
|
|
57
|
+
cursor: metadata.cursor,
|
|
58
|
+
sequence: metadata.sequence,
|
|
59
|
+
version: event.version,
|
|
60
|
+
updatedAt: event.updatedAt,
|
|
61
|
+
deletedAt: event.deletedAt ?? receivedAt,
|
|
62
|
+
}), options, receivedAt);
|
|
63
|
+
case "delta":
|
|
64
|
+
return boundTombstones(applyDelta(base, event.upserts ?? [], event.deletes ?? [], metadata), options, receivedAt);
|
|
65
|
+
case "heartbeat":
|
|
66
|
+
return {
|
|
67
|
+
...base,
|
|
68
|
+
status: base.status === "closed" || base.status === "error" ? base.status : "live",
|
|
69
|
+
lastHeartbeatAt: receivedAt,
|
|
70
|
+
staleSince: undefined,
|
|
71
|
+
error: base.status === "error" ? base.error : undefined,
|
|
72
|
+
statusReason: base.status === "error" ? base.statusReason : undefined,
|
|
73
|
+
retryAfterMs: base.status === "error" ? base.retryAfterMs : undefined,
|
|
74
|
+
reconnectAttempt: base.status === "error" ? base.reconnectAttempt : undefined,
|
|
75
|
+
};
|
|
76
|
+
case "status":
|
|
77
|
+
return {
|
|
78
|
+
...base,
|
|
79
|
+
status: event.status,
|
|
80
|
+
lastStatusAt: receivedAt,
|
|
81
|
+
staleSince: event.status === "stale" ? (event.staleSince ?? receivedAt) : undefined,
|
|
82
|
+
error: event.status === "error" ? base.error : undefined,
|
|
83
|
+
statusReason: event.reason,
|
|
84
|
+
retryAfterMs: event.retryAfterMs,
|
|
85
|
+
reconnectAttempt: event.reconnectAttempt,
|
|
86
|
+
terminalError: event.status === "error" ? base.terminalError : undefined,
|
|
87
|
+
};
|
|
88
|
+
case "error":
|
|
89
|
+
return {
|
|
90
|
+
...base,
|
|
91
|
+
status: event.terminal ? "error" : "reconnecting",
|
|
92
|
+
lastStatusAt: receivedAt,
|
|
93
|
+
error: event.error,
|
|
94
|
+
statusReason: event.code,
|
|
95
|
+
retryAfterMs: event.retryAfterMs,
|
|
96
|
+
terminalError: event.terminal === true,
|
|
97
|
+
staleSince: event.terminal ? base.staleSince : receivedAt,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function reconcileRealtimeStaleness(state, options) {
|
|
102
|
+
const now = options.now ?? Date.now();
|
|
103
|
+
const lastLiveAt = state.lastHeartbeatAt ?? state.lastEventAt;
|
|
104
|
+
if (!lastLiveAt || now - lastLiveAt <= options.staleAfterMs)
|
|
105
|
+
return state;
|
|
106
|
+
if (state.status === "closed" || state.status === "error" || state.status === "offline")
|
|
107
|
+
return state;
|
|
108
|
+
return {
|
|
109
|
+
...state,
|
|
110
|
+
status: "stale",
|
|
111
|
+
staleSince: state.staleSince ?? lastLiveAt + options.staleAfterMs,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function isDuplicateOrOutOfOrder(state, event) {
|
|
115
|
+
if (event.eventId && state.seenEventIds.includes(event.eventId))
|
|
116
|
+
return true;
|
|
117
|
+
const sequence = eventSequence(event);
|
|
118
|
+
return typeof sequence === "number" && typeof state.lastSequence === "number" && sequence <= state.lastSequence;
|
|
119
|
+
}
|
|
120
|
+
function withEventMetadata(state, event, receivedAt, maxSeenEventIds) {
|
|
121
|
+
const seenEventIds = event.eventId
|
|
122
|
+
? [...state.seenEventIds, event.eventId].slice(-Math.max(1, maxSeenEventIds))
|
|
123
|
+
: state.seenEventIds;
|
|
124
|
+
const sequence = eventSequence(event);
|
|
125
|
+
const checkpoint = mergeCheckpoint(state.checkpoint, event);
|
|
126
|
+
const cursor = eventCursor(event) ?? state.cursor;
|
|
127
|
+
const watermark = eventWatermark(event) ?? state.watermark;
|
|
128
|
+
const timestamp = eventTimestamp(event) ?? state.timestamp;
|
|
129
|
+
const deltaToken = eventDeltaToken(event) ?? state.deltaToken;
|
|
130
|
+
const status = isRecoverableStatus(state.status) ? "live" : state.status;
|
|
131
|
+
return {
|
|
132
|
+
...state,
|
|
133
|
+
status,
|
|
134
|
+
cursor,
|
|
135
|
+
watermark,
|
|
136
|
+
timestamp,
|
|
137
|
+
deltaToken,
|
|
138
|
+
checkpoint,
|
|
139
|
+
lastSequence: typeof sequence === "number" ? sequence : state.lastSequence,
|
|
140
|
+
lastEventAt: receivedAt,
|
|
141
|
+
staleSince: undefined,
|
|
142
|
+
error: status === "live" ? undefined : state.error,
|
|
143
|
+
seenEventIds,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function applySnapshot(state, features, metadata) {
|
|
147
|
+
const records = metadata.replace ? {} : { ...state.records };
|
|
148
|
+
const tombstones = metadata.replace ? {} : { ...state.tombstones };
|
|
149
|
+
for (const feature of features) {
|
|
150
|
+
const key = realtimeFeatureKey(feature.sourceId, feature.id);
|
|
151
|
+
records[key] = {
|
|
152
|
+
...feature,
|
|
153
|
+
key,
|
|
154
|
+
cursor: metadata.cursor,
|
|
155
|
+
sequence: metadata.sequence,
|
|
156
|
+
receivedAt: metadata.receivedAt,
|
|
157
|
+
};
|
|
158
|
+
delete tombstones[key];
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
...state,
|
|
162
|
+
records,
|
|
163
|
+
tombstones,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function applyDelta(state, upserts, deletes, metadata) {
|
|
167
|
+
if (upserts.length === 0 && deletes.length === 0) {
|
|
168
|
+
return state;
|
|
169
|
+
}
|
|
170
|
+
// Build the next records/tombstones maps once and mutate them across every
|
|
171
|
+
// item in the batch. The previous implementation delegated to
|
|
172
|
+
// applyUpsert/applyDelete per item, each of which spread the entire records
|
|
173
|
+
// (and tombstones) dictionary — O(deltaSize x stateSize): a 100-mutation
|
|
174
|
+
// delta over a 10k-feature state copied ~1M entries.
|
|
175
|
+
const records = { ...state.records };
|
|
176
|
+
const tombstones = { ...state.tombstones };
|
|
177
|
+
for (const feature of upserts) {
|
|
178
|
+
const key = realtimeFeatureKey(feature.sourceId, feature.id);
|
|
179
|
+
delete tombstones[key];
|
|
180
|
+
records[key] = {
|
|
181
|
+
...feature,
|
|
182
|
+
key,
|
|
183
|
+
cursor: metadata.cursor,
|
|
184
|
+
sequence: metadata.sequence,
|
|
185
|
+
receivedAt: metadata.receivedAt,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
for (const tombstone of deletes) {
|
|
189
|
+
const key = realtimeFeatureKey(tombstone.sourceId, tombstone.id);
|
|
190
|
+
delete records[key];
|
|
191
|
+
tombstones[key] = {
|
|
192
|
+
id: tombstone.id,
|
|
193
|
+
sourceId: tombstone.sourceId,
|
|
194
|
+
cursor: metadata.cursor,
|
|
195
|
+
sequence: metadata.sequence,
|
|
196
|
+
version: tombstone.version,
|
|
197
|
+
updatedAt: tombstone.updatedAt,
|
|
198
|
+
deletedAt: tombstone.deletedAt ?? metadata.receivedAt,
|
|
199
|
+
key,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
...state,
|
|
204
|
+
records,
|
|
205
|
+
tombstones,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Evict tombstones that exceed the configured age (`tombstoneTtlMs`) or count
|
|
210
|
+
* (`maxTombstones`, default {@link DEFAULT_MAX_TOMBSTONES}) bound. Returns the
|
|
211
|
+
* input state unchanged when nothing is pruned so reducers stay referentially
|
|
212
|
+
* stable on the common path.
|
|
213
|
+
*/
|
|
214
|
+
function boundTombstones(state, options, now) {
|
|
215
|
+
const maxTombstones = options.maxTombstones ?? DEFAULT_MAX_TOMBSTONES;
|
|
216
|
+
const ttlMs = options.tombstoneTtlMs;
|
|
217
|
+
const hasTtl = typeof ttlMs === "number" && ttlMs >= 0;
|
|
218
|
+
const entries = Object.values(state.tombstones);
|
|
219
|
+
const overCount = maxTombstones > 0 && entries.length > maxTombstones;
|
|
220
|
+
if (!overCount && !hasTtl) {
|
|
221
|
+
return state;
|
|
222
|
+
}
|
|
223
|
+
let kept = entries;
|
|
224
|
+
if (hasTtl) {
|
|
225
|
+
kept = kept.filter((tombstone) => now - (tombstone.deletedAt ?? 0) <= ttlMs);
|
|
226
|
+
}
|
|
227
|
+
if (maxTombstones > 0 && kept.length > maxTombstones) {
|
|
228
|
+
// Retain the most-recently-deleted entries.
|
|
229
|
+
kept = [...kept].sort((a, b) => (a.deletedAt ?? 0) - (b.deletedAt ?? 0)).slice(kept.length - maxTombstones);
|
|
230
|
+
}
|
|
231
|
+
if (kept.length === entries.length) {
|
|
232
|
+
return state;
|
|
233
|
+
}
|
|
234
|
+
const tombstones = {};
|
|
235
|
+
for (const tombstone of kept) {
|
|
236
|
+
tombstones[tombstone.key] = tombstone;
|
|
237
|
+
}
|
|
238
|
+
return { ...state, tombstones };
|
|
239
|
+
}
|
|
240
|
+
function applyUpsert(state, feature, metadata) {
|
|
241
|
+
const key = realtimeFeatureKey(feature.sourceId, feature.id);
|
|
242
|
+
const tombstones = { ...state.tombstones };
|
|
243
|
+
delete tombstones[key];
|
|
244
|
+
return {
|
|
245
|
+
...state,
|
|
246
|
+
records: {
|
|
247
|
+
...state.records,
|
|
248
|
+
[key]: {
|
|
249
|
+
...feature,
|
|
250
|
+
key,
|
|
251
|
+
cursor: metadata.cursor,
|
|
252
|
+
sequence: metadata.sequence,
|
|
253
|
+
receivedAt: metadata.receivedAt,
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
tombstones,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function applyDelete(state, tombstone) {
|
|
260
|
+
const key = realtimeFeatureKey(tombstone.sourceId, tombstone.id);
|
|
261
|
+
const records = { ...state.records };
|
|
262
|
+
delete records[key];
|
|
263
|
+
return {
|
|
264
|
+
...state,
|
|
265
|
+
records,
|
|
266
|
+
tombstones: {
|
|
267
|
+
...state.tombstones,
|
|
268
|
+
[key]: {
|
|
269
|
+
...tombstone,
|
|
270
|
+
key,
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function eventCursor(event) {
|
|
276
|
+
return event.checkpoint?.cursor ?? event.cursor;
|
|
277
|
+
}
|
|
278
|
+
function eventWatermark(event) {
|
|
279
|
+
return event.checkpoint?.watermark ?? event.watermark;
|
|
280
|
+
}
|
|
281
|
+
function eventTimestamp(event) {
|
|
282
|
+
return event.checkpoint?.timestamp ?? event.timestamp;
|
|
283
|
+
}
|
|
284
|
+
function eventDeltaToken(event) {
|
|
285
|
+
return event.checkpoint?.deltaToken ?? event.deltaToken;
|
|
286
|
+
}
|
|
287
|
+
function eventSequence(event) {
|
|
288
|
+
return event.sequence ?? event.checkpoint?.sequence;
|
|
289
|
+
}
|
|
290
|
+
function mutationMetadata(event, receivedAt) {
|
|
291
|
+
return {
|
|
292
|
+
cursor: eventCursor(event),
|
|
293
|
+
sequence: eventSequence(event),
|
|
294
|
+
receivedAt,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function mergeCheckpoint(previous, event) {
|
|
298
|
+
const cursor = eventCursor(event);
|
|
299
|
+
const watermark = eventWatermark(event);
|
|
300
|
+
const timestamp = eventTimestamp(event);
|
|
301
|
+
const sequence = eventSequence(event);
|
|
302
|
+
const deltaToken = eventDeltaToken(event);
|
|
303
|
+
if (previous === undefined &&
|
|
304
|
+
cursor === undefined &&
|
|
305
|
+
watermark === undefined &&
|
|
306
|
+
timestamp === undefined &&
|
|
307
|
+
sequence === undefined &&
|
|
308
|
+
deltaToken === undefined) {
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
...(previous ?? {}),
|
|
313
|
+
...(cursor !== undefined ? { cursor } : {}),
|
|
314
|
+
...(watermark !== undefined ? { watermark } : {}),
|
|
315
|
+
...(timestamp !== undefined ? { timestamp } : {}),
|
|
316
|
+
...(sequence !== undefined ? { sequence } : {}),
|
|
317
|
+
...(deltaToken !== undefined ? { deltaToken } : {}),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function isRecoverableStatus(status) {
|
|
321
|
+
return (status === "idle" ||
|
|
322
|
+
status === "connecting" ||
|
|
323
|
+
status === "reconnecting" ||
|
|
324
|
+
status === "stale" ||
|
|
325
|
+
status === "offline");
|
|
326
|
+
}
|
|
327
|
+
function encodeIdentityPart(value) {
|
|
328
|
+
if (value === undefined)
|
|
329
|
+
return "";
|
|
330
|
+
if (typeof value !== "object" || value === null)
|
|
331
|
+
return String(value);
|
|
332
|
+
try {
|
|
333
|
+
return JSON.stringify(sortIdentityValue(value));
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return String(value);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function sortIdentityValue(value) {
|
|
340
|
+
if (value instanceof Date)
|
|
341
|
+
return value.toISOString();
|
|
342
|
+
if (Array.isArray(value))
|
|
343
|
+
return value.map(sortIdentityValue);
|
|
344
|
+
if (typeof value !== "object" || value === null)
|
|
345
|
+
return value;
|
|
346
|
+
const sorted = {};
|
|
347
|
+
for (const key of Object.keys(value).sort()) {
|
|
348
|
+
sorted[key] = sortIdentityValue(value[key]);
|
|
349
|
+
}
|
|
350
|
+
return sorted;
|
|
351
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded, resumable SSE and WebSocket transports for `@honua/sdk-js/realtime`
|
|
3
|
+
* (issue #557), built on the #556 snapshot/delta/cursor/resume/plan-identity
|
|
4
|
+
* contract.
|
|
5
|
+
*
|
|
6
|
+
* `resumable.ts` deliberately never reconnects a transport — that ownership
|
|
7
|
+
* is explicitly deferred to this module (see
|
|
8
|
+
* `docs/decisions/realtime-snapshot-delta-cursor-resume-plan-identity-contract.md`,
|
|
9
|
+
* "the gate never reconnects a transport itself"). This module closes that
|
|
10
|
+
* gap by wrapping any raw {@link RealtimeFeatureTransport} (the SSE adapter
|
|
11
|
+
* in `sse.ts`, the WebSocket adapter in `websocket.ts`, or a custom one) with:
|
|
12
|
+
*
|
|
13
|
+
* - the bounded delivery gate from `resumable.ts` (`maxPendingEvents`,
|
|
14
|
+
* duplicate/gap detection, cross-scope checkpoint rejection);
|
|
15
|
+
* - reconnect ownership with exponential-backoff-and-jitter retry, bounded by
|
|
16
|
+
* `reconnect.maxAttempts` — exhausting attempts fails closed rather than
|
|
17
|
+
* retrying forever;
|
|
18
|
+
* - a heartbeat/liveness timeout that treats prolonged silence as a
|
|
19
|
+
* transport-detected gap;
|
|
20
|
+
* - resume-vs-resnapshot selection driven entirely by the gate's own
|
|
21
|
+
* `phase`: a reconnect resumes from the last accepted checkpoint's cursor
|
|
22
|
+
* only while the gate considers it valid, and requests a fresh snapshot
|
|
23
|
+
* the moment the gate (or this module, via `requireResnapshot`) decides
|
|
24
|
+
* otherwise — REQ-003's "resume only with a valid scoped cursor";
|
|
25
|
+
* - idempotent disposal that stops timers, the active connection, and the
|
|
26
|
+
* delivery gate exactly once, however disposal was triggered
|
|
27
|
+
* (caller `close()`, external `AbortSignal`, or an unrecoverable failure);
|
|
28
|
+
* - redacted telemetry (`onTelemetry`) built on `contract.ts`'s
|
|
29
|
+
* `deriveRealtimeContractAuthority` / `redactRealtimeCheckpoint`, so a raw
|
|
30
|
+
* cursor, watermark, or delta-token never reaches a log or metrics sink.
|
|
31
|
+
*
|
|
32
|
+
* The result still satisfies `RealtimeFeatureTransport`, so it composes with
|
|
33
|
+
* `createRealtimeFeatureStore` exactly like any other transport.
|
|
34
|
+
*
|
|
35
|
+
* @module
|
|
36
|
+
*/
|
|
37
|
+
import type { RealtimeContractAuthority, RedactedRealtimeCheckpointV1 } from "./contract.js";
|
|
38
|
+
import { type RealtimeCheckpointStore, type RealtimeDurableCheckpointV1, type RealtimeResumeContextV1 } from "./resumable.js";
|
|
39
|
+
import type { RealtimeServerSentEventsTransportOptions } from "./sse.js";
|
|
40
|
+
import type { RealtimeFeatureTransport } from "./types.js";
|
|
41
|
+
import type { RealtimeWebSocketTransportOptions } from "./websocket.js";
|
|
42
|
+
export interface RealtimeReconnectPolicy {
|
|
43
|
+
/** Consecutive reconnect attempts allowed before failing closed. `0` disables reconnect entirely. @default 8 */
|
|
44
|
+
readonly maxAttempts?: number;
|
|
45
|
+
/** Delay before the first reconnect attempt. @default 250 */
|
|
46
|
+
readonly baseDelayMs?: number;
|
|
47
|
+
/** Upper bound on the exponential backoff delay, before jitter. @default 30000 */
|
|
48
|
+
readonly maxDelayMs?: number;
|
|
49
|
+
/** How long a connection must stay live before a subsequent failure resets the attempt counter. @default 15000 */
|
|
50
|
+
readonly resetAfterMs?: number;
|
|
51
|
+
}
|
|
52
|
+
export interface RealtimeResumableTransportTelemetry {
|
|
53
|
+
readonly authority: RealtimeContractAuthority;
|
|
54
|
+
readonly reconnectAttempt: number;
|
|
55
|
+
readonly acceptedEventCount: number;
|
|
56
|
+
readonly duplicateEventCount: number;
|
|
57
|
+
readonly gapCount: number;
|
|
58
|
+
readonly overflowCount: number;
|
|
59
|
+
/** Never a raw cursor, watermark, or delta-token value. */
|
|
60
|
+
readonly checkpoint?: RedactedRealtimeCheckpointV1;
|
|
61
|
+
}
|
|
62
|
+
export interface RealtimeResumableTransportOptions<TFeature = unknown> {
|
|
63
|
+
readonly context: RealtimeResumeContextV1;
|
|
64
|
+
readonly checkpointStore?: RealtimeCheckpointStore;
|
|
65
|
+
/**
|
|
66
|
+
* Seed resume position for the very first connection, validated the same
|
|
67
|
+
* way a loaded `checkpointStore` checkpoint is (`evaluateRealtimeCheckpoint`).
|
|
68
|
+
* This is the only supported way to seed a resume position: `subscribe()`'s
|
|
69
|
+
* `request.cursor` / `resumeFrom` and similar fields are ignored by this
|
|
70
|
+
* wrapper so a stale or unscoped caller-supplied position can never bypass
|
|
71
|
+
* checkpoint compatibility validation.
|
|
72
|
+
*/
|
|
73
|
+
readonly initialCheckpoint?: RealtimeDurableCheckpointV1;
|
|
74
|
+
/** Includes the currently applying event. @default 64 */
|
|
75
|
+
readonly maxPendingEvents?: number;
|
|
76
|
+
/** Bounded event-id history persisted with each checkpoint. @default 256 */
|
|
77
|
+
readonly maxRecentEventIds?: number;
|
|
78
|
+
readonly reconnect?: RealtimeReconnectPolicy;
|
|
79
|
+
/** No event (including heartbeats) within this window is treated as a transport-detected gap. Disabled when omitted. */
|
|
80
|
+
readonly heartbeatTimeoutMs?: number;
|
|
81
|
+
/** Forwarded to {@link deriveRealtimeContractAuthority} for telemetry. */
|
|
82
|
+
readonly staleAfterMs?: number;
|
|
83
|
+
readonly onTelemetry?: (telemetry: RealtimeResumableTransportTelemetry) => void;
|
|
84
|
+
readonly now?: () => number;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Exponential backoff with jitter in the [0.5x, 1x] band for the given
|
|
88
|
+
* zero-based reconnect attempt — the same shape as
|
|
89
|
+
* `core/request-pipeline.ts`'s `resolveRetryDelayMs`, reimplemented locally
|
|
90
|
+
* so `@honua/sdk-js/realtime` does not pull in the HTTP request pipeline's
|
|
91
|
+
* error/type graph for one small helper. Exported so the schedule can be
|
|
92
|
+
* asserted deterministically without driving a fake transport end-to-end.
|
|
93
|
+
*/
|
|
94
|
+
export declare function computeReconnectDelayMs(policy: RealtimeReconnectPolicy | undefined, attempt: number): number;
|
|
95
|
+
/**
|
|
96
|
+
* Wrap a raw {@link RealtimeFeatureTransport} with bounded, resumable
|
|
97
|
+
* delivery: the #556 delivery gate, reconnect/backoff, heartbeat timeout,
|
|
98
|
+
* and redacted telemetry. See the module doc for the full behavior.
|
|
99
|
+
*/
|
|
100
|
+
export declare function createResumableRealtimeTransport<TFeature = unknown>(transport: RealtimeFeatureTransport<TFeature>, options: RealtimeResumableTransportOptions<TFeature>): RealtimeFeatureTransport<TFeature>;
|
|
101
|
+
/**
|
|
102
|
+
* Convenience factory combining {@link createRealtimeServerSentEventsTransport}
|
|
103
|
+
* with {@link createResumableRealtimeTransport}.
|
|
104
|
+
*/
|
|
105
|
+
export declare function createResumableServerSentEventsTransport<TFeature = unknown>(sseOptions: RealtimeServerSentEventsTransportOptions<TFeature>, resumableOptions: RealtimeResumableTransportOptions<TFeature>): RealtimeFeatureTransport<TFeature>;
|
|
106
|
+
/**
|
|
107
|
+
* Convenience factory combining {@link createRealtimeWebSocketTransport}
|
|
108
|
+
* with {@link createResumableRealtimeTransport}.
|
|
109
|
+
*/
|
|
110
|
+
export declare function createResumableWebSocketTransport<TFeature = unknown>(webSocketOptions: RealtimeWebSocketTransportOptions<TFeature>, resumableOptions: RealtimeResumableTransportOptions<TFeature>): RealtimeFeatureTransport<TFeature>;
|
|
111
|
+
//# sourceMappingURL=resumable-transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resumable-transport.d.ts","sourceRoot":"","sources":["../../../src/realtime/resumable-transport.ts"],"names":[],"mappings":"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"}
|