@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,504 @@
1
+ /**
2
+ * Durable {@link RealtimeCheckpointStore} implementations for
3
+ * `@honua/sdk-js/realtime` (issue #937).
4
+ *
5
+ * `resumable.ts` already owns the resume state machine: it loads a checkpoint,
6
+ * validates it with {@link evaluateRealtimeCheckpoint}, and requires an explicit
7
+ * replacement snapshot whenever the checkpoint no longer applies. What it never
8
+ * had was a shipped store, so a reload always resnapshotted even when a valid
9
+ * cursor was in hand a second earlier. This module supplies that durability
10
+ * without redefining any cursor semantics:
11
+ *
12
+ * - {@link createIndexedDbRealtimeCheckpointStore} persists one record per
13
+ * resume scope in the browser, keyed by the authorization-scope digest,
14
+ * source identity, and query identity that already define checkpoint scope.
15
+ * - {@link createMemoryRealtimeCheckpointStore} runs the identical
16
+ * normalization, screening, expiry, and eviction logic without IndexedDB, for
17
+ * tests and non-persistent hosts.
18
+ * - {@link createRealtimeCheckpointStore} binds that same logic to any
19
+ * {@link RealtimeCheckpointRecordStorage}, for hosts this module ships no
20
+ * adapter for.
21
+ *
22
+ * Four properties are deliberate:
23
+ *
24
+ * 1. **Cursors only.** A record carries the resume position (sequence, cursor,
25
+ * watermark, timestamp, delta token), the scope identity it belongs to, and
26
+ * two observation times. It never carries features, snapshots, request URLs,
27
+ * headers, or credentials. The authorization scope is reduced to a SHA-256
28
+ * digest before it reaches storage, and every remaining persisted string is
29
+ * screened with the shared persisted-string screen in
30
+ * `src/connect-url-safety.ts` — the same discipline the offline region store
31
+ * applies through `assertCredentialFreeManifest`.
32
+ * 2. **Fail-closed resume.** Load revalidates through
33
+ * {@link evaluateRealtimeCheckpoint} and discards the record outright when
34
+ * the source, query, source version, schema version, or authorization scope
35
+ * changed, when the record is corrupt, or when the checkpoint is older than
36
+ * the configured maximum age. A discarded checkpoint means the subscription
37
+ * resnapshots; it never means a partial delta stream is delivered as though
38
+ * it were complete.
39
+ * 3. **Bounded and atomic.** Each scope holds exactly one record (last write
40
+ * wins), the store holds at most `maxRecords` scope records, and the
41
+ * IndexedDB write plus its eviction sweep share a single readwrite
42
+ * transaction.
43
+ * 4. **Never throws into a subscription.** A storage failure, a refused
44
+ * credential-shaped value, or a corrupt record resolves and reports a
45
+ * {@link RealtimeCheckpointStoreDiagnosticV1} carrying a
46
+ * `HonuaRealtimeResumeError`, so the subscription degrades to a resnapshot
47
+ * instead of entering the gate's terminal `error` phase.
48
+ *
49
+ * The store stays opt-in: nothing here is wired into a subscription unless a
50
+ * caller passes it as `checkpointStore`, so no existing subscription changes
51
+ * behaviour by default.
52
+ *
53
+ * @module
54
+ */
55
+ import { credentialScreenMessage, screenPersistedString } from "../connect-url-safety.js";
56
+ import { canonicalStringify, sha256 } from "../query-planner/canonical.js";
57
+ import { HonuaRealtimeResumeError, REALTIME_DURABLE_CHECKPOINT_VERSION, evaluateRealtimeCheckpoint, } from "./resumable.js";
58
+ /**
59
+ * Versioned format string stamped on every persisted record. A record whose
60
+ * format string differs is treated as corrupt and discarded on load, the same
61
+ * posture the offline stores take, so a future format can never be
62
+ * misinterpreted as this one.
63
+ */
64
+ export const HONUA_REALTIME_CHECKPOINT_STORE_FORMAT = "honua.realtime-checkpoint-store/1.0";
65
+ /**
66
+ * Default maximum checkpoint age. A cursor older than this forces a resnapshot
67
+ * even when its scope still matches, because a server may retire a cursor
68
+ * window without telling the client; a silent gap is worse than an honest
69
+ * resnapshot.
70
+ */
71
+ export const DEFAULT_REALTIME_CHECKPOINT_MAX_AGE_MS = 15 * 60 * 1000;
72
+ /** Absolute ceiling on `maxAgeMs`. Callers may tighten it, never raise it. */
73
+ export const MAX_REALTIME_CHECKPOINT_MAX_AGE_MS = 24 * 60 * 60 * 1000;
74
+ /**
75
+ * Default cap on distinct resume scopes retained per store. Each scope holds at
76
+ * most one record, so this is also the record cap; the least recently written
77
+ * record is evicted first.
78
+ */
79
+ export const DEFAULT_REALTIME_CHECKPOINT_MAX_RECORDS = 64;
80
+ /** Absolute ceiling on `maxRecords`. Callers may tighten it, never raise it. */
81
+ export const MAX_REALTIME_CHECKPOINT_RECORDS = 512;
82
+ const DATABASE_VERSION = 1;
83
+ const CHECKPOINT_STORE = "checkpoints";
84
+ const OBSERVED_AT_INDEX = "observedAt";
85
+ /** Reason codes mapped onto the delivery gate's existing failure taxonomy. */
86
+ const DIAGNOSTIC_ERROR_CODES = {
87
+ "invalid-checkpoint": "invalid-checkpoint",
88
+ "source-changed": "source-changed",
89
+ "query-changed": "query-changed",
90
+ "source-version-changed": "source-version-changed",
91
+ "schema-version-changed": "schema-version-changed",
92
+ "authorization-scope-changed": "authorization-scope-changed",
93
+ "checkpoint-expired": "cursor-expired",
94
+ "credential-screened": "checkpoint-save-failed",
95
+ "storage-failed": "checkpoint-load-failed",
96
+ "unsupported-format": "invalid-checkpoint",
97
+ };
98
+ /**
99
+ * Deterministic scope key for a resume context: a digest over the
100
+ * authorization-scope digest, source identity, and query identity — exactly the
101
+ * identities checkpoint scope validation is defined against. Source and schema
102
+ * versions are intentionally excluded so a version bump replaces the record in
103
+ * place and is discarded explicitly on load, instead of orphaning it under a
104
+ * key nothing will read again.
105
+ */
106
+ export function realtimeCheckpointScopeKey(context) {
107
+ return sha256(`honua-realtime-checkpoint-scope:v1:${canonicalStringify({
108
+ authorizationScopeDigest: realtimeAuthorizationScopeDigest(context.authorizationScopeFingerprint),
109
+ queryFingerprint: String(context.queryFingerprint),
110
+ sourceId: String(context.sourceId),
111
+ })}`);
112
+ }
113
+ /**
114
+ * Opaque digest of an authorization-scope fingerprint. The fingerprint is
115
+ * already documented as a non-secret opaque identity; persisting only its
116
+ * digest keeps that promise enforceable rather than assumed.
117
+ */
118
+ export function realtimeAuthorizationScopeDigest(fingerprint) {
119
+ return sha256(`honua-realtime-checkpoint-scope-digest:v1:${fingerprint}`);
120
+ }
121
+ /**
122
+ * Bind the shared scope, screening, expiry, and eviction logic to any
123
+ * {@link RealtimeCheckpointRecordStorage}. The returned store never throws into
124
+ * a subscription: a rejected storage call, a refused value, or a corrupt record
125
+ * resolves and reports a diagnostic, so the subscription resnapshots.
126
+ */
127
+ export function createRealtimeCheckpointStore(storage, options = {}) {
128
+ const normalized = normalizeOptions(options);
129
+ return {
130
+ load: (context, signal) => loadCheckpoint(storage, normalized, context, signal),
131
+ save: (checkpoint, signal) => saveCheckpoint(storage, normalized, checkpoint, signal),
132
+ records: async () => (await storage.list()).filter((record) => isCheckpointRecord(record)),
133
+ clear: () => storage.clear(),
134
+ };
135
+ }
136
+ /** In-memory store with the persistent store's exact semantics, for tests and non-persistent hosts. */
137
+ export function createMemoryRealtimeCheckpointStore(options = {}) {
138
+ return createRealtimeCheckpointStore(createMemoryCheckpointStorage(), options);
139
+ }
140
+ /** Map-backed storage with the same one-record-per-scope, oldest-first eviction contract. */
141
+ export function createMemoryCheckpointStorage() {
142
+ const records = new Map();
143
+ return {
144
+ read: async (key) => records.get(key),
145
+ write: async (record, maxRecords) => {
146
+ // Delete first so the re-inserted key moves to the tail of the insertion
147
+ // order, which is the eviction order.
148
+ records.delete(record.key);
149
+ records.set(record.key, record);
150
+ for (const key of [...records.keys()].slice(0, Math.max(0, records.size - maxRecords)))
151
+ records.delete(key);
152
+ },
153
+ remove: async (key) => {
154
+ records.delete(key);
155
+ },
156
+ list: async () => [...records.values()],
157
+ clear: async () => records.clear(),
158
+ };
159
+ }
160
+ /** Persistent browser store: one bounded, atomically written record per resume scope. */
161
+ export function createIndexedDbRealtimeCheckpointStore(options = {}) {
162
+ return createRealtimeCheckpointStore(createIndexedDbCheckpointStorage(options), options);
163
+ }
164
+ /** IndexedDB storage; each method runs in exactly one transaction. */
165
+ export function createIndexedDbCheckpointStorage(options = {}) {
166
+ const factory = options.indexedDB ?? globalThis.indexedDB;
167
+ if (!factory)
168
+ throw new Error("IndexedDB is not available in this runtime.");
169
+ const name = options.name ?? "honua-realtime-checkpoints";
170
+ if (name.trim().length === 0)
171
+ throw new Error("IndexedDB database name must be non-empty.");
172
+ const database = openDatabase(factory, name);
173
+ return {
174
+ read: async (key) => runTransaction(await database, "readonly", async (transaction) => request(transaction.objectStore(CHECKPOINT_STORE).get(key))),
175
+ write: async (record, maxRecords) => runTransaction(await database, "readwrite", async (transaction) => {
176
+ const store = transaction.objectStore(CHECKPOINT_STORE);
177
+ store.put(record);
178
+ const total = await request(store.count());
179
+ if (total > maxRecords)
180
+ await evictOldest(store, total - maxRecords, record.key);
181
+ }),
182
+ remove: async (key) => runTransaction(await database, "readwrite", async (transaction) => {
183
+ transaction.objectStore(CHECKPOINT_STORE).delete(key);
184
+ }),
185
+ list: async () => runTransaction(await database, "readonly", async (transaction) => request(transaction.objectStore(CHECKPOINT_STORE).getAll())),
186
+ clear: async () => runTransaction(await database, "readwrite", async (transaction) => {
187
+ transaction.objectStore(CHECKPOINT_STORE).clear();
188
+ }),
189
+ };
190
+ }
191
+ async function loadCheckpoint(backend, options, context, signal) {
192
+ if (signal?.aborted)
193
+ return undefined;
194
+ let scopeKey;
195
+ try {
196
+ scopeKey = realtimeCheckpointScopeKey(context);
197
+ }
198
+ catch (cause) {
199
+ // A context this store cannot even key is not a resumable context.
200
+ report(options, "load", "invalid-checkpoint", INVALID_SCOPE_KEY, "Resume context could not be keyed.", cause);
201
+ return undefined;
202
+ }
203
+ let stored;
204
+ try {
205
+ stored = await backend.read(scopeKey);
206
+ }
207
+ catch (cause) {
208
+ report(options, "load", "storage-failed", scopeKey, "Unable to read the stored realtime checkpoint.", cause);
209
+ return undefined;
210
+ }
211
+ if (stored === undefined || signal?.aborted)
212
+ return undefined;
213
+ const projection = projectStoredCheckpoint(stored, context, options);
214
+ if ("reason" in projection) {
215
+ await discard(backend, options, scopeKey, projection.reason, projection.detail);
216
+ return undefined;
217
+ }
218
+ return projection.checkpoint;
219
+ }
220
+ async function saveCheckpoint(backend, options, checkpoint, signal) {
221
+ if (signal?.aborted)
222
+ return;
223
+ let scopeKey = INVALID_SCOPE_KEY;
224
+ try {
225
+ const compatibility = evaluateRealtimeCheckpoint(checkpoint?.context, checkpoint);
226
+ if (!compatibility.compatible) {
227
+ report(options, "save", "invalid-checkpoint", scopeKey, compatibility.reason);
228
+ return;
229
+ }
230
+ scopeKey = realtimeCheckpointScopeKey(checkpoint.context);
231
+ }
232
+ catch (cause) {
233
+ report(options, "save", "invalid-checkpoint", scopeKey, "Checkpoint could not be projected for storage.", cause);
234
+ return;
235
+ }
236
+ const record = projectRecord(checkpoint, scopeKey, options.now());
237
+ if ("path" in record) {
238
+ // Refusing to persist is deliberate: the in-memory checkpoint still drives
239
+ // this connection, and the next load simply resnapshots.
240
+ report(options, "save", "credential-screened", scopeKey, credentialScreenMessage(record.path, record.reason));
241
+ return;
242
+ }
243
+ try {
244
+ await backend.write(record.record, options.maxRecords);
245
+ }
246
+ catch (cause) {
247
+ report(options, "save", "storage-failed", scopeKey, "Unable to persist the realtime checkpoint.", cause);
248
+ }
249
+ }
250
+ async function discard(backend, options, scopeKey, reason, detail) {
251
+ try {
252
+ await backend.remove(scopeKey);
253
+ }
254
+ catch {
255
+ // The record is already unusable; failing to delete it must not change the
256
+ // outcome, which is an explicit resnapshot either way.
257
+ }
258
+ report(options, "load", reason, scopeKey, detail);
259
+ }
260
+ function projectStoredCheckpoint(stored, context, options) {
261
+ // The format stamp is read before anything else, so a record from another
262
+ // store layout is reported as the version fact it is instead of being
263
+ // absorbed into the generic corrupt bucket. The offending value is never
264
+ // echoed: a hostile record could put anything in that field.
265
+ if (typeof stored === "object" && stored !== null && !isStoreFormat(stored.format)) {
266
+ return {
267
+ reason: "unsupported-format",
268
+ detail: `Stored realtime checkpoint was written in a different store format; this build reads "${HONUA_REALTIME_CHECKPOINT_STORE_FORMAT}".`,
269
+ };
270
+ }
271
+ if (!isCheckpointRecord(stored)) {
272
+ return { reason: "invalid-checkpoint", detail: "Stored realtime checkpoint record is corrupt or unreadable." };
273
+ }
274
+ if (stored.authorizationScopeDigest !== realtimeAuthorizationScopeDigest(context.authorizationScopeFingerprint)) {
275
+ return {
276
+ reason: "authorization-scope-changed",
277
+ detail: "Stored checkpoint authorization scope does not match the current grants.",
278
+ };
279
+ }
280
+ const ageMs = options.now() - Date.parse(stored.savedAt);
281
+ if (!Number.isFinite(ageMs) || ageMs < 0 || ageMs > options.maxAgeMs) {
282
+ return {
283
+ reason: "checkpoint-expired",
284
+ detail: `Stored checkpoint is outside the ${options.maxAgeMs}ms maximum resume age.`,
285
+ };
286
+ }
287
+ const checkpoint = Object.freeze({
288
+ kind: "honua.realtime-checkpoint",
289
+ version: REALTIME_DURABLE_CHECKPOINT_VERSION,
290
+ context: Object.freeze({
291
+ kind: "honua.realtime-resume-context",
292
+ version: 1,
293
+ sourceId: stored.sourceId,
294
+ queryFingerprint: stored.queryFingerprint,
295
+ sourceVersion: stored.sourceVersion,
296
+ schemaVersion: stored.schemaVersion,
297
+ // Safe only because the digest above already matched this exact scope.
298
+ authorizationScopeFingerprint: context.authorizationScopeFingerprint,
299
+ }),
300
+ resume: Object.freeze({ ...stored.resume }),
301
+ recentEventIds: Object.freeze([]),
302
+ savedAt: stored.savedAt,
303
+ });
304
+ const compatibility = evaluateRealtimeCheckpoint(context, checkpoint);
305
+ if (!compatibility.compatible) {
306
+ // `compatible` is only reported alongside `compatible: true`; an
307
+ // incompatible result that still carried it would be a contract break, so
308
+ // it degrades to the shape-level rejection rather than resuming.
309
+ const reason = compatibility.code === "compatible" ? "invalid-checkpoint" : compatibility.code;
310
+ return { reason, detail: compatibility.reason };
311
+ }
312
+ return { checkpoint };
313
+ }
314
+ function projectRecord(checkpoint, scopeKey, now) {
315
+ const context = checkpoint.context;
316
+ const resume = {
317
+ sequence: checkpoint.resume.sequence,
318
+ ...optionalText("cursor", checkpoint.resume),
319
+ ...optionalText("watermark", checkpoint.resume),
320
+ ...optionalText("timestamp", checkpoint.resume),
321
+ ...optionalText("deltaToken", checkpoint.resume),
322
+ };
323
+ const screened = [
324
+ ["sourceId", String(context.sourceId)],
325
+ ["queryFingerprint", context.queryFingerprint],
326
+ ["sourceVersion", context.sourceVersion],
327
+ ["schemaVersion", context.schemaVersion],
328
+ ...(resume.cursor === undefined ? [] : [["resume.cursor", resume.cursor]]),
329
+ ...(resume.watermark === undefined ? [] : [["resume.watermark", resume.watermark]]),
330
+ ...(resume.timestamp === undefined ? [] : [["resume.timestamp", resume.timestamp]]),
331
+ ...(resume.deltaToken === undefined ? [] : [["resume.deltaToken", resume.deltaToken]]),
332
+ ];
333
+ for (const [path, value] of screened) {
334
+ const reason = screenPersistedString(value, "identity");
335
+ if (reason)
336
+ return { path, reason };
337
+ }
338
+ const observedAt = new Date(Number.isFinite(now) ? now : Date.now()).toISOString();
339
+ return {
340
+ record: {
341
+ key: scopeKey,
342
+ format: HONUA_REALTIME_CHECKPOINT_STORE_FORMAT,
343
+ sourceId: String(context.sourceId),
344
+ queryFingerprint: context.queryFingerprint,
345
+ sourceVersion: context.sourceVersion,
346
+ schemaVersion: context.schemaVersion,
347
+ authorizationScopeDigest: realtimeAuthorizationScopeDigest(context.authorizationScopeFingerprint),
348
+ resume,
349
+ savedAt: checkpoint.savedAt,
350
+ observedAt,
351
+ },
352
+ };
353
+ }
354
+ function optionalText(name, resume) {
355
+ const value = resume[name];
356
+ return typeof value === "string" ? { [name]: value } : {};
357
+ }
358
+ function report(options, operation, reason, scopeKey, detail, cause) {
359
+ if (!options.onDiagnostic)
360
+ return;
361
+ const error = new HonuaRealtimeResumeError(DIAGNOSTIC_ERROR_CODES[reason], detail, cause === undefined ? undefined : { cause });
362
+ options.onDiagnostic(Object.freeze({
363
+ kind: "honua.realtime-checkpoint-store-diagnostic",
364
+ version: 1,
365
+ operation,
366
+ reason,
367
+ scopeKey,
368
+ error,
369
+ }));
370
+ }
371
+ function normalizeOptions(options) {
372
+ return {
373
+ maxAgeMs: tightenedPositive(options.maxAgeMs, DEFAULT_REALTIME_CHECKPOINT_MAX_AGE_MS, MAX_REALTIME_CHECKPOINT_MAX_AGE_MS, "maxAgeMs"),
374
+ maxRecords: tightenedPositive(options.maxRecords, DEFAULT_REALTIME_CHECKPOINT_MAX_RECORDS, MAX_REALTIME_CHECKPOINT_RECORDS, "maxRecords"),
375
+ now: options.now ?? Date.now,
376
+ ...(options.onDiagnostic ? { onDiagnostic: options.onDiagnostic } : {}),
377
+ };
378
+ }
379
+ function tightenedPositive(value, fallback, ceiling, name) {
380
+ if (value === undefined)
381
+ return fallback;
382
+ if (!Number.isSafeInteger(value) || value < 1) {
383
+ throw new HonuaRealtimeResumeError("invalid-checkpoint", `${name} must be a safe integer greater than zero.`);
384
+ }
385
+ if (value > ceiling) {
386
+ throw new HonuaRealtimeResumeError("invalid-checkpoint", `${name} cannot exceed the ${ceiling} safety ceiling.`);
387
+ }
388
+ return value;
389
+ }
390
+ const INVALID_SCOPE_KEY = `sha256:${"0".repeat(64)}`;
391
+ function isStoreFormat(value) {
392
+ return value === HONUA_REALTIME_CHECKPOINT_STORE_FORMAT;
393
+ }
394
+ function isCheckpointRecord(value) {
395
+ if (typeof value !== "object" || value === null)
396
+ return false;
397
+ const record = value;
398
+ if (!isStoreFormat(record.format))
399
+ return false;
400
+ for (const name of ["key", "sourceId", "queryFingerprint", "sourceVersion", "schemaVersion"]) {
401
+ if (typeof record[name] !== "string" || record[name].length === 0)
402
+ return false;
403
+ }
404
+ if (typeof record.authorizationScopeDigest !== "string" || !record.authorizationScopeDigest.startsWith("sha256:")) {
405
+ return false;
406
+ }
407
+ if (typeof record.savedAt !== "string" || !Number.isFinite(Date.parse(record.savedAt)))
408
+ return false;
409
+ if (typeof record.observedAt !== "string" || !Number.isFinite(Date.parse(record.observedAt)))
410
+ return false;
411
+ if (typeof record.resume !== "object" || record.resume === null)
412
+ return false;
413
+ const resume = record.resume;
414
+ if (!Number.isSafeInteger(resume.sequence) || resume.sequence < 0)
415
+ return false;
416
+ for (const name of ["cursor", "watermark", "timestamp", "deltaToken"]) {
417
+ if (resume[name] !== undefined && typeof resume[name] !== "string")
418
+ return false;
419
+ }
420
+ return true;
421
+ }
422
+ function openDatabase(factory, name) {
423
+ return new Promise((resolve, reject) => {
424
+ const open = factory.open(name, DATABASE_VERSION);
425
+ open.onupgradeneeded = () => {
426
+ const database = open.result;
427
+ const store = database.objectStoreNames.contains(CHECKPOINT_STORE)
428
+ ? open.transaction?.objectStore(CHECKPOINT_STORE)
429
+ : database.createObjectStore(CHECKPOINT_STORE, { keyPath: "key" });
430
+ if (store && !store.indexNames.contains(OBSERVED_AT_INDEX))
431
+ store.createIndex(OBSERVED_AT_INDEX, "observedAt");
432
+ };
433
+ open.onsuccess = () => resolve(open.result);
434
+ open.onerror = () => reject(open.error ?? new Error("Failed to open the realtime checkpoint database."));
435
+ open.onblocked = () => reject(new Error("Realtime checkpoint database upgrade is blocked by another connection."));
436
+ });
437
+ }
438
+ function runTransaction(database, mode, body) {
439
+ return new Promise((resolve, reject) => {
440
+ const transaction = database.transaction([CHECKPOINT_STORE], mode);
441
+ let result;
442
+ let settled = false;
443
+ void body(transaction).then((value) => {
444
+ result = value;
445
+ }, (error) => {
446
+ settled = true;
447
+ try {
448
+ transaction.abort();
449
+ }
450
+ catch {
451
+ // The transaction may already have completed or aborted.
452
+ }
453
+ reject(error);
454
+ });
455
+ transaction.oncomplete = () => {
456
+ if (settled)
457
+ return;
458
+ settled = true;
459
+ resolve(result);
460
+ };
461
+ transaction.onerror = () => {
462
+ if (settled)
463
+ return;
464
+ settled = true;
465
+ reject(transaction.error ?? new Error("Realtime checkpoint transaction failed."));
466
+ };
467
+ transaction.onabort = () => {
468
+ if (settled)
469
+ return;
470
+ settled = true;
471
+ reject(transaction.error ?? new Error("Realtime checkpoint transaction aborted."));
472
+ };
473
+ });
474
+ }
475
+ function request(source) {
476
+ return new Promise((resolve, reject) => {
477
+ source.onsuccess = () => resolve(source.result);
478
+ source.onerror = () => reject(source.error ?? new Error("Realtime checkpoint request failed."));
479
+ });
480
+ }
481
+ /** Delete `count` least recently written records inside the caller's transaction. */
482
+ function evictOldest(store, count, protectedKey) {
483
+ return new Promise((resolve, reject) => {
484
+ let remaining = count;
485
+ if (remaining <= 0) {
486
+ resolve();
487
+ return;
488
+ }
489
+ const cursorRequest = store.index(OBSERVED_AT_INDEX).openCursor();
490
+ cursorRequest.onerror = () => reject(cursorRequest.error ?? new Error("Failed to enumerate realtime checkpoint records."));
491
+ cursorRequest.onsuccess = () => {
492
+ const cursor = cursorRequest.result;
493
+ if (!cursor || remaining <= 0) {
494
+ resolve();
495
+ return;
496
+ }
497
+ if (cursor.primaryKey !== protectedKey) {
498
+ cursor.delete();
499
+ remaining -= 1;
500
+ }
501
+ cursor.continue();
502
+ };
503
+ });
504
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Snapshot, delta, cursor, resume, and plan-identity contract surface for
3
+ * `@honua/sdk-js/realtime` (issue #556).
4
+ *
5
+ * The resumable delivery gate in `resumable.ts` already implements the
6
+ * durable-checkpoint state machine: versioned snapshot/delta/cursor
7
+ * envelopes, monotonic-sequence ordering and deduplication, delete
8
+ * tombstones (`reducer.ts`), and mandatory resnapshot on gap, expiry, or
9
+ * scope change. This module closes the three remaining contract gaps that
10
+ * keep that machinery a *design* rather than an ad hoc convention:
11
+ *
12
+ * 1. **Plan identity** — {@link RealtimeResumeContextV1.queryFingerprint} was
13
+ * previously any caller-supplied string. {@link realtimePlanFingerprint}
14
+ * and {@link assertRealtimePlanIdentity} bind it to the query planner's
15
+ * canonical, credential-free `QueryExecutionPlan.fingerprint` — the same
16
+ * content hash `hashQueryPlan`/`persistedPlanSnapshot` establish once,
17
+ * with full protocol-compiler re-verification, when a plan is first
18
+ * constructed (`explainQuery`) or reloaded (`parseQueryPlan`). That
19
+ * fingerprint already folds in `capabilityPolicy` and `fallback`, so
20
+ * binding to it also carries policy identity per REQ-003 without a
21
+ * second fingerprint field. This module reads the already-trusted field
22
+ * rather than importing `hashQueryPlan` itself: plan *integrity* is the
23
+ * query planner's trust boundary, established once at construction time,
24
+ * and re-deriving it here would pull every protocol compiler
25
+ * (DuckDB/gRPC/GeoServices/OGC/OData/WFS — several hundred KB) into the
26
+ * `@honua/sdk-js/realtime` bundle for a check that is already done.
27
+ * 2. **Authority** — {@link deriveRealtimeContractAuthority} projects the
28
+ * resumable gate's `phase` into one explicit
29
+ * `replaying | live | stale | terminal` state plus an `authoritative`
30
+ * flag, so consumers stop inferring trust from reason codes.
31
+ * 3. **Deterministic, redacted serialization** — {@link serializeRealtimeCheckpoint}
32
+ * and {@link redactRealtimeCheckpoint} give a canonical (sorted-key) JSON
33
+ * form for durable storage and a form safe for logs/telemetry that never
34
+ * carries a raw cursor, watermark, or delta-token value.
35
+ *
36
+ * @module
37
+ */
38
+ import type { QueryExecutionPlan } from "../query-planner/types.js";
39
+ import { type RealtimeDurableCheckpointV1, type RealtimeResumeContextV1, type ResumableRealtimeState } from "./resumable.js";
40
+ /**
41
+ * Canonical plan-identity fingerprint for
42
+ * {@link RealtimeResumeContextV1.queryFingerprint}: the accepted plan's own
43
+ * trusted `fingerprint` field (`hashQueryPlan(plan) === plan.fingerprint`
44
+ * for every plan produced by `explainQuery`/`parseQueryPlan`). Named and
45
+ * exported so call sites bind resume identity to the plan by construction
46
+ * instead of by convention, without importing the query planner's
47
+ * protocol-compiler graph into `@honua/sdk-js/realtime`. Only
48
+ * `QueryExecutionPlan["fingerprint"]` is type-imported; nothing from
49
+ * `query-planner/planner.js` is bundled here.
50
+ */
51
+ export declare function realtimePlanFingerprint(plan: Pick<QueryExecutionPlan, "fingerprint">): `sha256:${string}`;
52
+ /**
53
+ * Assert that a resume context's `queryFingerprint` matches the accepted
54
+ * plan's fingerprint. Throws a `"query-changed"`
55
+ * {@link HonuaRealtimeResumeError} — the same taxonomy
56
+ * {@link evaluateRealtimeCheckpoint} uses for a checkpoint whose bound query
57
+ * identity no longer matches — when they diverge, so a subscription can
58
+ * never resume against a plan other than the one it was accepted for. This
59
+ * checks identity, not plan authenticity: trust that `plan.fingerprint`
60
+ * matches the plan's actual content is established once, by the query
61
+ * planner, when the plan is constructed or parsed.
62
+ */
63
+ export declare function assertRealtimePlanIdentity(context: RealtimeResumeContextV1, plan: Pick<QueryExecutionPlan, "fingerprint">): void;
64
+ /**
65
+ * Explicit realtime contract authority states.
66
+ *
67
+ * - `replaying`: no confirmed authoritative baseline yet — awaiting an
68
+ * initial or replacement snapshot, or resuming from a durable checkpoint
69
+ * whose continuity has not been reconfirmed by a live event.
70
+ * - `live`: an accepted snapshot establishes ground truth and the most
71
+ * recent contiguous event arrived within the caller's staleness window
72
+ * (or no window was supplied).
73
+ * - `stale`: the baseline is still authoritative but no event has arrived
74
+ * within `staleAfterMs`; render the last known state but surface
75
+ * liveness doubt.
76
+ * - `terminal`: delivery stopped on an unrecoverable error or an explicit
77
+ * close. The last accepted checkpoint, if any, remains available for
78
+ * read-only rendering but will never advance.
79
+ */
80
+ export type RealtimeContractAuthorityState = "replaying" | "live" | "stale" | "terminal";
81
+ export interface RealtimeContractAuthority {
82
+ readonly state: RealtimeContractAuthorityState;
83
+ /** True once an accepted snapshot has established ground truth for the current checkpoint identity. */
84
+ readonly authoritative: boolean;
85
+ /** Milliseconds since the checkpoint was accepted, when a checkpoint exists and `now` was supplied. */
86
+ readonly ageMs?: number;
87
+ }
88
+ export interface DeriveRealtimeContractAuthorityOptions {
89
+ /** No event within this window while `phase === "live"` demotes authority to `"stale"`. */
90
+ readonly staleAfterMs?: number;
91
+ readonly now?: number;
92
+ }
93
+ /**
94
+ * Derive one explicit {@link RealtimeContractAuthority} from a resumable
95
+ * gate's state. Pure and side-effect free: call it after every
96
+ * `enqueue`/`requireResnapshot`/`close` transition to render connection
97
+ * chrome without re-deriving trust from `phase`/`reason` combinations.
98
+ */
99
+ export declare function deriveRealtimeContractAuthority(resumable: Pick<ResumableRealtimeState, "phase" | "checkpoint">, options?: DeriveRealtimeContractAuthorityOptions): RealtimeContractAuthority;
100
+ /** Opaque, deterministic digest of a raw resume-position value; never the raw value itself. */
101
+ export type RedactedResumePosition = `sha256:${string}`;
102
+ export interface RedactedRealtimeResumePosition {
103
+ readonly sequence: number;
104
+ readonly cursor?: RedactedResumePosition;
105
+ readonly watermark?: RedactedResumePosition;
106
+ readonly timestamp?: string;
107
+ readonly deltaToken?: RedactedResumePosition;
108
+ }
109
+ /**
110
+ * Log/telemetry-safe projection of a durable checkpoint. `context` is kept
111
+ * as-is because every one of its fields is already an opaque identity or
112
+ * fingerprint (never a credential). `resume.cursor`, `resume.watermark`, and
113
+ * `resume.deltaToken` are replaced with a deterministic SHA-256 digest so
114
+ * repeated observations of the same position stay correlatable without
115
+ * exposing the raw, potentially sensitive token. `timestamp` is kept in the
116
+ * clear because it is not an opaque credential. `recentEventIds` is
117
+ * collapsed to a count: individual event ids are dedup identity, not
118
+ * something a log line needs verbatim.
119
+ */
120
+ export interface RedactedRealtimeCheckpointV1 {
121
+ readonly kind: "honua.realtime-checkpoint";
122
+ readonly version: RealtimeDurableCheckpointV1["version"];
123
+ readonly context: RealtimeResumeContextV1;
124
+ readonly resume: RedactedRealtimeResumePosition;
125
+ readonly recentEventIdCount: number;
126
+ readonly savedAt: string;
127
+ }
128
+ export declare function redactRealtimeCheckpoint(checkpoint: RealtimeDurableCheckpointV1): RedactedRealtimeCheckpointV1;
129
+ /**
130
+ * Canonical (sorted-key) JSON serialization of the full durable checkpoint,
131
+ * for persistence where the caller owns access control. Two checkpoints
132
+ * with the same accepted content always serialize identically, regardless
133
+ * of property insertion order — required for content-addressed storage and
134
+ * for stable diffing across processes.
135
+ */
136
+ export declare function serializeRealtimeCheckpoint(checkpoint: RealtimeDurableCheckpointV1): string;
137
+ /**
138
+ * Canonical, redacted JSON serialization safe for logs and telemetry
139
+ * sinks — never a raw cursor, watermark, or delta-token value.
140
+ */
141
+ export declare function serializeRedactedRealtimeCheckpoint(checkpoint: RealtimeDurableCheckpointV1): string;
142
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../../src/realtime/contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,EAEL,KAAK,2BAA2B,EAChC,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC5B,MAAM,gBAAgB,CAAC;AAExB;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,IAAI,CAAC,kBAAkB,EAAE,aAAa,CAAC,GAAG,UAAU,MAAM,EAAE,CAEzG;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,uBAAuB,EAChC,IAAI,EAAE,IAAI,CAAC,kBAAkB,EAAE,aAAa,CAAC,GAC5C,IAAI,CAQN;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,8BAA8B,GAAG,WAAW,GAAG,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC;AAEzF,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,KAAK,EAAE,8BAA8B,CAAC;IAC/C,uGAAuG;IACvG,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,uGAAuG;IACvG,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,sCAAsC;IACrD,2FAA2F;IAC3F,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAC7C,SAAS,EAAE,IAAI,CAAC,sBAAsB,EAAE,OAAO,GAAG,YAAY,CAAC,EAC/D,OAAO,GAAE,sCAA2C,GACnD,yBAAyB,CA2B3B;AAYD,+FAA+F;AAC/F,MAAM,MAAM,sBAAsB,GAAG,UAAU,MAAM,EAAE,CAAC;AAExD,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,sBAAsB,CAAC;IACzC,QAAQ,CAAC,SAAS,CAAC,EAAE,sBAAsB,CAAC;IAC5C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,CAAC,EAAE,sBAAsB,CAAC;CAC9C;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,IAAI,EAAE,2BAA2B,CAAC;IAC3C,QAAQ,CAAC,OAAO,EAAE,2BAA2B,CAAC,SAAS,CAAC,CAAC;IACzD,QAAQ,CAAC,OAAO,EAAE,uBAAuB,CAAC;IAC1C,QAAQ,CAAC,MAAM,EAAE,8BAA8B,CAAC;IAChD,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,2BAA2B,GAAG,4BAA4B,CAmB9G;AAMD;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,2BAA2B,GAAG,MAAM,CAE3F;AAED;;;GAGG;AACH,wBAAgB,mCAAmC,CAAC,UAAU,EAAE,2BAA2B,GAAG,MAAM,CAEnG"}