@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.
Files changed (295) hide show
  1. package/contract/discovery.d.ts +34 -0
  2. package/contract/discovery.d.ts.map +1 -1
  3. package/contract/index.d.ts +7 -5
  4. package/contract/index.d.ts.map +1 -1
  5. package/contract/index.js +3 -2
  6. package/contract/pmtiles.d.ts +10 -1
  7. package/contract/pmtiles.d.ts.map +1 -1
  8. package/contract/pmtiles.js +10 -2
  9. package/contract/protocol-module.d.ts +84 -9
  10. package/contract/protocol-module.d.ts.map +1 -1
  11. package/contract/query-filter-fes.d.ts +18 -0
  12. package/contract/query-filter-fes.d.ts.map +1 -0
  13. package/contract/query-filter-fes.js +122 -0
  14. package/contract/query-filter.d.ts +300 -0
  15. package/contract/query-filter.d.ts.map +1 -0
  16. package/contract/query-filter.js +883 -0
  17. package/contract/schema.d.ts +12 -0
  18. package/contract/schema.d.ts.map +1 -1
  19. package/contract/schema.js +34 -0
  20. package/contract/source-capability-support.d.ts.map +1 -1
  21. package/contract/source-capability-support.js +15 -3
  22. package/contract/source.d.ts +45 -25
  23. package/contract/source.d.ts.map +1 -1
  24. package/contract/source.js +719 -479
  25. package/contract/types.d.ts +88 -19
  26. package/contract/types.d.ts.map +1 -1
  27. package/contract/types.js +4 -0
  28. package/contract/widget-source.d.ts.map +1 -1
  29. package/core/client.d.ts +51 -21
  30. package/core/client.d.ts.map +1 -1
  31. package/core/client.js +234 -54
  32. package/core/error-classifications.d.ts +23 -0
  33. package/core/error-classifications.d.ts.map +1 -1
  34. package/core/error-classifications.js +23 -0
  35. package/core/error-code-registry.d.ts +24 -1
  36. package/core/error-code-registry.d.ts.map +1 -1
  37. package/core/error-code-registry.js +23 -0
  38. package/core/error-envelope.js +2 -0
  39. package/core/errors.d.ts +2 -2
  40. package/core/errors.d.ts.map +1 -1
  41. package/core/errors.js +1 -1
  42. package/core/geoparquet-sql.d.ts +69 -5
  43. package/core/geoparquet-sql.d.ts.map +1 -1
  44. package/core/geoparquet-sql.js +327 -10
  45. package/core/odata-write-codec.d.ts.map +1 -1
  46. package/core/odata-write-codec.js +0 -1
  47. package/core/odata.d.ts +6 -0
  48. package/core/odata.d.ts.map +1 -1
  49. package/core/odata.js +6 -1
  50. package/core/ogc-processes.d.ts +182 -25
  51. package/core/ogc-processes.d.ts.map +1 -1
  52. package/core/ogc-processes.js +602 -30
  53. package/core/process-runner.d.ts +8 -1
  54. package/core/process-runner.d.ts.map +1 -1
  55. package/core/process-runner.js +1 -0
  56. package/core/protocol-transport.d.ts +2 -0
  57. package/core/protocol-transport.d.ts.map +1 -1
  58. package/core/stac.d.ts +32 -0
  59. package/core/stac.d.ts.map +1 -1
  60. package/core/stac.js +158 -23
  61. package/core/surfaces.d.ts.map +1 -1
  62. package/core/surfaces.js +273 -68
  63. package/core/types.d.ts +138 -11
  64. package/core/types.d.ts.map +1 -1
  65. package/core/wfs-axis-order.d.ts +18 -0
  66. package/core/wfs-axis-order.d.ts.map +1 -0
  67. package/core/wfs-axis-order.js +108 -0
  68. package/core/wfs-capabilities.d.ts +65 -24
  69. package/core/wfs-capabilities.d.ts.map +1 -1
  70. package/core/wfs-capabilities.js +237 -27
  71. package/core/wfs-filter.d.ts +26 -3
  72. package/core/wfs-filter.d.ts.map +1 -1
  73. package/core/wfs-filter.js +48 -53
  74. package/core/wfs-protocol-error.d.ts +12 -0
  75. package/core/wfs-protocol-error.d.ts.map +1 -0
  76. package/core/wfs-protocol-error.js +13 -0
  77. package/core/wfs.d.ts +53 -1
  78. package/core/wfs.d.ts.map +1 -1
  79. package/core/wfs.js +251 -29
  80. package/core/wms-feature-info.d.ts +92 -0
  81. package/core/wms-feature-info.d.ts.map +1 -0
  82. package/core/wms-feature-info.js +285 -0
  83. package/core/wms.d.ts +9 -0
  84. package/core/wms.d.ts.map +1 -1
  85. package/core/wms.js +17 -5
  86. package/esri-compat/event-bus.d.ts +20 -0
  87. package/esri-compat/event-bus.d.ts.map +1 -1
  88. package/esri-compat/feature-filter.d.ts +46 -1
  89. package/esri-compat/feature-filter.d.ts.map +1 -1
  90. package/esri-compat/feature-filter.js +49 -0
  91. package/esri-compat/geojson-layer.d.ts +4 -2
  92. package/esri-compat/geojson-layer.d.ts.map +1 -1
  93. package/esri-compat/geojson-layer.js +4 -2
  94. package/esri-compat/identity-manager.js +2 -2
  95. package/esri-compat/imagery-layer.d.ts +2 -1
  96. package/esri-compat/imagery-layer.d.ts.map +1 -1
  97. package/esri-compat/imagery-layer.js +2 -1
  98. package/esri-compat/layer-list.d.ts +8 -1
  99. package/esri-compat/layer-list.d.ts.map +1 -1
  100. package/esri-compat/layer-list.js +11 -4
  101. package/esri-compat/legend.d.ts.map +1 -1
  102. package/esri-compat/legend.js +1 -1
  103. package/esri-compat/locator.d.ts +297 -0
  104. package/esri-compat/locator.d.ts.map +1 -0
  105. package/esri-compat/locator.js +579 -0
  106. package/esri-compat/map-image-layer.d.ts.map +1 -1
  107. package/esri-compat/map-view.d.ts.map +1 -1
  108. package/esri-compat/map-view.js +1 -1
  109. package/esri-compat/scene-view.d.ts.map +1 -1
  110. package/esri-compat/time-slider.d.ts +29 -0
  111. package/esri-compat/time-slider.d.ts.map +1 -1
  112. package/esri-compat/time-slider.js +117 -0
  113. package/esri-compat/wfs-layer.d.ts +2 -1
  114. package/esri-compat/wfs-layer.d.ts.map +1 -1
  115. package/esri-compat/wfs-layer.js +2 -1
  116. package/esri-compat/widget-host.d.ts +42 -6
  117. package/esri-compat/widget-host.d.ts.map +1 -1
  118. package/esri-compat/widget-host.js +99 -10
  119. package/generated-app/projection.d.ts.map +1 -1
  120. package/generated-app/runtime.d.ts.map +1 -1
  121. package/interactions/declarative-contract.d.ts +149 -0
  122. package/interactions/declarative-contract.d.ts.map +1 -0
  123. package/interactions/declarative-contract.js +293 -0
  124. package/interactions/declarative.d.ts +198 -0
  125. package/interactions/declarative.d.ts.map +1 -0
  126. package/interactions/declarative.js +470 -0
  127. package/interactions/feature-state.d.ts +7 -0
  128. package/interactions/feature-state.d.ts.map +1 -1
  129. package/interactions/feature-state.js +10 -2
  130. package/map/data-to-map-bridge.d.ts +1 -1
  131. package/map/data-to-map-bridge.d.ts.map +1 -1
  132. package/map/data-to-map-bridge.js +1 -2
  133. package/map/honua-map.d.ts.map +1 -1
  134. package/map/honua-map.js +1 -1
  135. package/map/index.d.ts +2 -0
  136. package/map/index.d.ts.map +1 -1
  137. package/map/index.js +2 -0
  138. package/map/maplibre-target.js +1 -1
  139. package/map/realtime-reconciliation-adapter.d.ts +98 -0
  140. package/map/realtime-reconciliation-adapter.d.ts.map +1 -0
  141. package/map/realtime-reconciliation-adapter.js +84 -0
  142. package/map/temporal-playback.d.ts +25 -0
  143. package/map/temporal-playback.d.ts.map +1 -1
  144. package/map/temporal-playback.js +29 -1
  145. package/package.json +4 -3
  146. package/query-planner/canonical-filter.d.ts +30 -0
  147. package/query-planner/canonical-filter.d.ts.map +1 -0
  148. package/query-planner/canonical-filter.js +30 -0
  149. package/query-planner/columnar-plan.d.ts +63 -0
  150. package/query-planner/columnar-plan.d.ts.map +1 -0
  151. package/query-planner/columnar-plan.js +94 -0
  152. package/query-planner/diagnostics.d.ts +3 -3
  153. package/query-planner/diagnostics.d.ts.map +1 -1
  154. package/query-planner/diagnostics.js +17 -4
  155. package/query-planner/duckdb.d.ts.map +1 -1
  156. package/query-planner/duckdb.js +16 -2
  157. package/query-planner/execution-context.d.ts.map +1 -1
  158. package/query-planner/execution-context.js +3 -1
  159. package/query-planner/executor.d.ts.map +1 -1
  160. package/query-planner/executor.js +7 -1
  161. package/query-planner/geoservices.d.ts.map +1 -1
  162. package/query-planner/geoservices.js +24 -5
  163. package/query-planner/grpc.d.ts.map +1 -1
  164. package/query-planner/grpc.js +29 -5
  165. package/query-planner/index.d.ts +20 -5
  166. package/query-planner/index.d.ts.map +1 -1
  167. package/query-planner/index.js +16 -3
  168. package/query-planner/ir.d.ts +6 -1
  169. package/query-planner/ir.d.ts.map +1 -1
  170. package/query-planner/ir.js +63 -1
  171. package/query-planner/odata-protocol-module.d.ts +54 -0
  172. package/query-planner/odata-protocol-module.d.ts.map +1 -0
  173. package/query-planner/odata-protocol-module.js +199 -0
  174. package/query-planner/odata-v1.d.ts +23 -0
  175. package/query-planner/odata-v1.d.ts.map +1 -0
  176. package/query-planner/odata-v1.js +202 -0
  177. package/query-planner/odata.d.ts +0 -3
  178. package/query-planner/odata.d.ts.map +1 -1
  179. package/query-planner/odata.js +0 -143
  180. package/query-planner/ogc-features.d.ts.map +1 -1
  181. package/query-planner/ogc-features.js +23 -1
  182. package/query-planner/planner.d.ts +2 -2
  183. package/query-planner/planner.d.ts.map +1 -1
  184. package/query-planner/planner.js +146 -27
  185. package/query-planner/representation.d.ts +59 -0
  186. package/query-planner/representation.d.ts.map +1 -0
  187. package/query-planner/representation.js +140 -0
  188. package/query-planner/types.d.ts +153 -3
  189. package/query-planner/types.d.ts.map +1 -1
  190. package/query-planner/wfs-protocol-module.d.ts +57 -0
  191. package/query-planner/wfs-protocol-module.d.ts.map +1 -0
  192. package/query-planner/wfs-protocol-module.js +731 -0
  193. package/query-planner/wfs-v1.d.ts +41 -0
  194. package/query-planner/wfs-v1.d.ts.map +1 -0
  195. package/query-planner/wfs-v1.js +366 -0
  196. package/query-planner/wfs.d.ts +1 -3
  197. package/query-planner/wfs.d.ts.map +1 -1
  198. package/query-planner/wfs.js +1 -70
  199. package/react/honua-map.d.ts +3 -1
  200. package/react/honua-map.d.ts.map +1 -1
  201. package/react/honua-map.js +11 -1
  202. package/react/hooks.d.ts +1 -1
  203. package/react/hooks.js +1 -1
  204. package/react/selection.d.ts.map +1 -1
  205. package/react/selection.js +0 -0
  206. package/react/use-mounted-source.d.ts +1 -1
  207. package/react/use-mounted-source.js +1 -1
  208. package/realtime/checkpoint-store.d.ts +206 -0
  209. package/realtime/checkpoint-store.d.ts.map +1 -0
  210. package/realtime/checkpoint-store.js +504 -0
  211. package/realtime/contract.d.ts +142 -0
  212. package/realtime/contract.d.ts.map +1 -0
  213. package/realtime/contract.js +154 -0
  214. package/realtime/exploration.d.ts +16 -0
  215. package/realtime/exploration.d.ts.map +1 -0
  216. package/realtime/exploration.js +32 -0
  217. package/realtime/honua-server.d.ts +131 -0
  218. package/realtime/honua-server.d.ts.map +1 -0
  219. package/realtime/honua-server.js +302 -0
  220. package/realtime/index.d.ts +39 -0
  221. package/realtime/index.d.ts.map +1 -0
  222. package/realtime/index.js +26 -0
  223. package/realtime/odata-delta.d.ts +114 -0
  224. package/realtime/odata-delta.d.ts.map +1 -0
  225. package/realtime/odata-delta.js +483 -0
  226. package/realtime/projections.d.ts +30 -0
  227. package/realtime/projections.d.ts.map +1 -0
  228. package/realtime/projections.js +51 -0
  229. package/realtime/reconciliation.d.ts +245 -0
  230. package/realtime/reconciliation.d.ts.map +1 -0
  231. package/realtime/reconciliation.js +377 -0
  232. package/realtime/reducer.d.ts +14 -0
  233. package/realtime/reducer.d.ts.map +1 -0
  234. package/realtime/reducer.js +351 -0
  235. package/realtime/resumable-transport.d.ts +111 -0
  236. package/realtime/resumable-transport.d.ts.map +1 -0
  237. package/realtime/resumable-transport.js +406 -0
  238. package/realtime/resumable.d.ts +104 -0
  239. package/realtime/resumable.d.ts.map +1 -0
  240. package/realtime/resumable.js +632 -0
  241. package/realtime/sse.d.ts +23 -0
  242. package/realtime/sse.d.ts.map +1 -0
  243. package/realtime/sse.js +153 -0
  244. package/realtime/store.d.ts +18 -0
  245. package/realtime/store.d.ts.map +1 -0
  246. package/realtime/store.js +63 -0
  247. package/realtime/types.d.ts +190 -0
  248. package/realtime/types.d.ts.map +1 -0
  249. package/realtime/types.js +10 -0
  250. package/realtime/websocket.d.ts +60 -0
  251. package/realtime/websocket.d.ts.map +1 -0
  252. package/realtime/websocket.js +157 -0
  253. package/realtime/wire.d.ts +28 -0
  254. package/realtime/wire.d.ts.map +1 -0
  255. package/realtime/wire.js +73 -0
  256. package/runtime/index.d.ts +6 -2
  257. package/runtime/index.d.ts.map +1 -1
  258. package/runtime/index.js +3 -1
  259. package/runtime/load-package.d.ts +24 -0
  260. package/runtime/load-package.d.ts.map +1 -1
  261. package/runtime/load-package.js +45 -1
  262. package/runtime/maplibre-renderer.d.ts +11 -0
  263. package/runtime/maplibre-renderer.d.ts.map +1 -1
  264. package/runtime/maplibre-renderer.js +32 -7
  265. package/runtime/offline-region-protocol.d.ts +73 -0
  266. package/runtime/offline-region-protocol.d.ts.map +1 -0
  267. package/runtime/offline-region-protocol.js +63 -0
  268. package/runtime/offline-region-style.d.ts +78 -0
  269. package/runtime/offline-region-style.d.ts.map +1 -0
  270. package/runtime/offline-region-style.js +147 -0
  271. package/runtime/pmtiles-protocol.d.ts +7 -8
  272. package/runtime/pmtiles-protocol.d.ts.map +1 -1
  273. package/runtime/pmtiles-protocol.js +19 -66
  274. package/runtime/protocol-registry.d.ts +55 -0
  275. package/runtime/protocol-registry.d.ts.map +1 -0
  276. package/runtime/protocol-registry.js +103 -0
  277. package/runtime/runtime.d.ts +2 -0
  278. package/runtime/runtime.d.ts.map +1 -1
  279. package/runtime/runtime.js +6 -1
  280. package/runtime/style-compose.d.ts.map +1 -1
  281. package/runtime/style-compose.js +78 -9
  282. package/studio/index.d.ts +24 -7
  283. package/studio/index.d.ts.map +1 -1
  284. package/studio/index.js +21 -7
  285. package/studio/lifecycle-client.d.ts +164 -0
  286. package/studio/lifecycle-client.d.ts.map +1 -0
  287. package/studio/lifecycle-client.js +299 -0
  288. package/studio/lifecycle-errors.d.ts +91 -0
  289. package/studio/lifecycle-errors.d.ts.map +1 -0
  290. package/studio/lifecycle-errors.js +77 -0
  291. package/studio/lifecycle-types.d.ts +321 -0
  292. package/studio/lifecycle-types.d.ts.map +1 -0
  293. package/studio/lifecycle-types.js +22 -0
  294. package/webmap/convert-label.js +0 -1
  295. 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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"}