@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,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"}
|