@honua/app-platform 0.1.12 → 0.1.14
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/columnar/apache-arrow.d.ts +17 -0
- package/columnar/apache-arrow.d.ts.map +1 -0
- package/columnar/apache-arrow.js +1036 -0
- package/columnar/batch-cache-conformance.d.ts +66 -0
- package/columnar/batch-cache-conformance.d.ts.map +1 -0
- package/columnar/batch-cache-conformance.js +317 -0
- package/columnar/batch-cache-indexeddb.d.ts +31 -0
- package/columnar/batch-cache-indexeddb.d.ts.map +1 -0
- package/columnar/batch-cache-indexeddb.js +148 -0
- package/columnar/batch-cache.d.ts +298 -0
- package/columnar/batch-cache.d.ts.map +1 -0
- package/columnar/batch-cache.js +757 -0
- package/columnar/geoarrow-aggregate.d.ts +178 -0
- package/columnar/geoarrow-aggregate.d.ts.map +1 -0
- package/columnar/geoarrow-aggregate.js +976 -0
- package/columnar/geoarrow-patch.d.ts +382 -0
- package/columnar/geoarrow-patch.d.ts.map +1 -0
- package/columnar/geoarrow-patch.js +1656 -0
- package/columnar/geoarrow-reproject.d.ts +22 -0
- package/columnar/geoarrow-reproject.d.ts.map +1 -0
- package/columnar/geoarrow-reproject.js +119 -0
- package/columnar/geoarrow-serialization.d.ts +68 -0
- package/columnar/geoarrow-serialization.d.ts.map +1 -0
- package/columnar/geoarrow-serialization.js +331 -0
- package/columnar/geoarrow-types.d.ts +271 -0
- package/columnar/geoarrow-types.d.ts.map +1 -0
- package/columnar/geoarrow-types.js +15 -0
- package/columnar/geoarrow.d.ts +52 -0
- package/columnar/geoarrow.d.ts.map +1 -0
- package/columnar/geoarrow.js +1469 -0
- package/columnar/index.d.ts +43 -0
- package/columnar/index.d.ts.map +1 -0
- package/columnar/index.js +29 -0
- package/columnar/operators.d.ts +70 -0
- package/columnar/operators.d.ts.map +1 -0
- package/columnar/operators.js +307 -0
- package/columnar/result.d.ts +369 -0
- package/columnar/result.d.ts.map +1 -0
- package/columnar/result.js +815 -0
- package/columnar/telemetry.d.ts +152 -0
- package/columnar/telemetry.d.ts.map +1 -0
- package/columnar/telemetry.js +265 -0
- package/columnar/transfer.d.ts +28 -0
- package/columnar/transfer.d.ts.map +1 -0
- package/columnar/transfer.js +669 -0
- package/columnar/types.d.ts +175 -0
- package/columnar/types.d.ts.map +1 -0
- package/columnar/types.js +22 -0
- package/columnar/worker.d.ts +157 -0
- package/columnar/worker.d.ts.map +1 -0
- package/columnar/worker.js +1059 -0
- package/contract/pmtiles.d.ts +6 -5
- package/contract/pmtiles.d.ts.map +1 -1
- package/contract/pmtiles.js +30 -5
- package/contract/source.d.ts.map +1 -1
- package/contract/source.js +11 -8
- package/controls/catalog.d.ts.map +1 -1
- package/controls/catalog.js +110 -0
- package/controls/index.d.ts +1 -1
- package/controls/index.js +1 -1
- package/controls/types.d.ts +1 -1
- package/controls/types.js +1 -1
- package/core/client.d.ts.map +1 -1
- package/core/client.js +87 -19
- package/core/deprecation-notice.d.ts +28 -0
- package/core/deprecation-notice.d.ts.map +1 -0
- package/core/deprecation-notice.js +22 -0
- package/core/esri-geojson.d.ts.map +1 -1
- package/core/esri-geojson.js +14 -8
- package/core/optional-grpc-peer.d.ts +15 -0
- package/core/optional-grpc-peer.d.ts.map +1 -0
- package/core/optional-grpc-peer.js +60 -0
- package/esri-compat/basemap-gallery.d.ts +2 -0
- package/esri-compat/basemap-gallery.d.ts.map +1 -1
- package/esri-compat/basemap-gallery.js +12 -0
- package/esri-compat/basemap.d.ts +4 -0
- package/esri-compat/basemap.d.ts.map +1 -1
- package/esri-compat/basemap.js +70 -0
- package/esri-compat/bookmarks.d.ts +2 -0
- package/esri-compat/bookmarks.d.ts.map +1 -1
- package/esri-compat/bookmarks.js +12 -0
- package/esri-compat/controls.d.ts +18 -0
- package/esri-compat/controls.d.ts.map +1 -1
- package/esri-compat/controls.js +120 -57
- package/esri-compat/directions.d.ts +4 -0
- package/esri-compat/directions.d.ts.map +1 -1
- package/esri-compat/directions.js +30 -20
- package/esri-compat/editor.d.ts +4 -0
- package/esri-compat/editor.d.ts.map +1 -1
- package/esri-compat/editor.js +16 -0
- package/esri-compat/esri-config.d.ts +2 -1
- package/esri-compat/esri-config.d.ts.map +1 -1
- package/esri-compat/feature-effect.d.ts +23 -0
- package/esri-compat/feature-effect.d.ts.map +1 -0
- package/esri-compat/feature-effect.js +12 -0
- package/esri-compat/feature-form.d.ts +6 -0
- package/esri-compat/feature-form.d.ts.map +1 -1
- package/esri-compat/feature-form.js +63 -0
- package/esri-compat/feature-layer.d.ts +21 -2
- package/esri-compat/feature-layer.d.ts.map +1 -1
- package/esri-compat/feature-layer.js +172 -1
- package/esri-compat/feature-table.d.ts +5 -0
- package/esri-compat/feature-table.d.ts.map +1 -1
- package/esri-compat/feature-table.js +23 -0
- package/esri-compat/identity-manager.d.ts +1 -1
- package/esri-compat/identity-manager.js +1 -1
- package/esri-compat/legend.d.ts +2 -0
- package/esri-compat/legend.d.ts.map +1 -1
- package/esri-compat/legend.js +98 -0
- package/esri-compat/locator.d.ts +1 -1
- package/esri-compat/locator.js +1 -1
- package/esri-compat/map-view-mount.d.ts +1 -0
- package/esri-compat/map-view-mount.d.ts.map +1 -1
- package/esri-compat/map-view-mount.js +46 -25
- package/esri-compat/map-view.d.ts +13 -0
- package/esri-compat/map-view.d.ts.map +1 -1
- package/esri-compat/map-view.js +94 -8
- package/esri-compat/map.d.ts +5 -1
- package/esri-compat/map.d.ts.map +1 -1
- package/esri-compat/map.js +10 -0
- package/esri-compat/measurement.d.ts +2 -0
- package/esri-compat/measurement.d.ts.map +1 -1
- package/esri-compat/measurement.js +14 -0
- package/esri-compat/point.d.ts +7 -0
- package/esri-compat/point.d.ts.map +1 -1
- package/esri-compat/point.js +33 -3
- package/esri-compat/popup.d.ts +2 -0
- package/esri-compat/popup.d.ts.map +1 -1
- package/esri-compat/popup.js +12 -0
- package/esri-compat/portal.d.ts +1 -1
- package/esri-compat/portal.js +1 -1
- package/esri-compat/print.d.ts +3 -0
- package/esri-compat/print.d.ts.map +1 -1
- package/esri-compat/print.js +15 -0
- package/esri-compat/search.d.ts +4 -0
- package/esri-compat/search.d.ts.map +1 -1
- package/esri-compat/search.js +25 -28
- package/esri-compat/sketch.d.ts +2 -0
- package/esri-compat/sketch.d.ts.map +1 -1
- package/esri-compat/sketch.js +12 -0
- package/esri-compat/widget-host.d.ts +46 -17
- package/esri-compat/widget-host.d.ts.map +1 -1
- package/esri-compat/widget-host.js +84 -20
- package/esri-compat/widget-shell-hosts.d.ts +61 -0
- package/esri-compat/widget-shell-hosts.d.ts.map +1 -0
- package/esri-compat/widget-shell-hosts.js +76 -0
- package/kernel/renderer.d.ts +82 -0
- package/kernel/renderer.js +1 -0
- package/offline/conflict-resolution.d.ts +183 -0
- package/offline/conflict-resolution.d.ts.map +1 -0
- package/offline/conflict-resolution.js +286 -0
- package/offline/credential-screen.d.ts +34 -0
- package/offline/credential-screen.d.ts.map +1 -0
- package/offline/credential-screen.js +78 -0
- package/offline/digest.d.ts +15 -0
- package/offline/digest.d.ts.map +1 -0
- package/offline/digest.js +58 -0
- package/offline/edit-queue.d.ts +403 -0
- package/offline/edit-queue.d.ts.map +1 -0
- package/offline/edit-queue.js +1807 -0
- package/offline/edit-replay.d.ts +116 -0
- package/offline/edit-replay.d.ts.map +1 -0
- package/offline/edit-replay.js +392 -0
- package/offline/fetch-handler.d.ts +12 -0
- package/offline/fetch-handler.d.ts.map +1 -0
- package/offline/fetch-handler.js +65 -0
- package/offline/index.d.ts +52 -0
- package/offline/index.d.ts.map +1 -0
- package/offline/index.js +34 -0
- package/offline/indexeddb.d.ts +135 -0
- package/offline/indexeddb.d.ts.map +1 -0
- package/offline/indexeddb.js +888 -0
- package/offline/memory-store.d.ts +17 -0
- package/offline/memory-store.d.ts.map +1 -0
- package/offline/memory-store.js +183 -0
- package/offline/quota.d.ts +161 -0
- package/offline/quota.d.ts.map +1 -0
- package/offline/quota.js +370 -0
- package/offline/read-gate.d.ts +175 -0
- package/offline/read-gate.d.ts.map +1 -0
- package/offline/read-gate.js +252 -0
- package/offline/read-resource.d.ts +99 -0
- package/offline/read-resource.d.ts.map +1 -0
- package/offline/read-resource.js +156 -0
- package/offline/read.d.ts +50 -0
- package/offline/read.d.ts.map +1 -0
- package/offline/read.js +65 -0
- package/offline/region.d.ts +29 -0
- package/offline/region.d.ts.map +1 -0
- package/offline/region.js +1007 -0
- package/offline/replay-conflict.d.ts +183 -0
- package/offline/replay-conflict.d.ts.map +1 -0
- package/offline/replay-conflict.js +482 -0
- package/offline/selection.d.ts +140 -0
- package/offline/selection.d.ts.map +1 -0
- package/offline/selection.js +391 -0
- package/offline/snapshot.d.ts +132 -0
- package/offline/snapshot.d.ts.map +1 -0
- package/offline/snapshot.js +312 -0
- package/offline/status.d.ts +136 -0
- package/offline/status.d.ts.map +1 -0
- package/offline/status.js +479 -0
- package/offline/store-conformance.d.ts +48 -0
- package/offline/store-conformance.d.ts.map +1 -0
- package/offline/store-conformance.js +233 -0
- package/offline/tile-protocol.d.ts +81 -0
- package/offline/tile-protocol.d.ts.map +1 -0
- package/offline/tile-protocol.js +119 -0
- package/offline/types.d.ts +349 -0
- package/offline/types.d.ts.map +1 -0
- package/offline/types.js +75 -0
- package/package.json +14 -2
- package/realtime/odata-delta.d.ts +1 -1
- package/realtime/odata-delta.d.ts.map +1 -1
- package/realtime/odata-delta.js +38 -10
- package/scene-workspace/index.d.ts +2 -2
- package/scene-workspace/index.d.ts.map +1 -1
- package/scene-workspace/index.js +1 -1
- package/scene-workspace/scene-discovery.d.ts +111 -91
- package/scene-workspace/scene-discovery.d.ts.map +1 -1
- package/scene-workspace/scene-discovery.js +340 -92
- package/scene-workspace/scene-view.d.ts +6 -6
- package/scene-workspace/scene-view.d.ts.map +1 -1
- package/scene-workspace/scene-view.js +32 -6
- package/web-components/elements.d.ts +34 -0
- package/web-components/elements.d.ts.map +1 -1
- package/web-components/elements.js +82 -8
- package/web-components/index.d.ts +2 -0
- package/web-components/index.d.ts.map +1 -1
- package/web-components/index.js +3 -1
- package/web-components/map-chrome.d.ts +103 -0
- package/web-components/map-chrome.d.ts.map +1 -0
- package/web-components/map-chrome.js +325 -0
- package/web-components/widget-shell.d.ts +68 -0
- package/web-components/widget-shell.d.ts.map +1 -0
- package/web-components/widget-shell.js +224 -0
- package/widget-capabilities.d.ts +67 -0
- package/widget-capabilities.js +152 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { QueryTileKey, QueryTileKeyInput, QueryTileScheme } from "../contract/tiles.js";
|
|
2
|
+
import type { Query } from "../contract/types.js";
|
|
3
|
+
import { type OfflineRegionBounds, type OfflineRegionResourceKind } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Protocol-neutral selection identity for offline reads.
|
|
6
|
+
*
|
|
7
|
+
* A persisted region is addressed by what the caller asked for — a source, a
|
|
8
|
+
* canonical query, a bounded extent, an authorization scope, and the source /
|
|
9
|
+
* schema / plan versions it was captured at — never by a request URL. Snapshot
|
|
10
|
+
* planning and the read path both derive resource identity here, so a read can
|
|
11
|
+
* only ever find bytes that were stored for exactly that selection.
|
|
12
|
+
*
|
|
13
|
+
* @experimental
|
|
14
|
+
*/
|
|
15
|
+
/** Protocol name reported by capability failures raised on the offline read path. */
|
|
16
|
+
export declare const OFFLINE_REGION_PROTOCOL: "offline-region";
|
|
17
|
+
/**
|
|
18
|
+
* Kind-specific discriminator that separates several resources of one kind
|
|
19
|
+
* inside a single selection: tile coordinates, an asset key, or a document name.
|
|
20
|
+
* Values are credential-screened and persisted only inside a digest.
|
|
21
|
+
*/
|
|
22
|
+
export type OfflineRegionResourceSelector = string | number | readonly (string | number)[];
|
|
23
|
+
/**
|
|
24
|
+
* The pagination-free canonical form of a {@link Query}.
|
|
25
|
+
*
|
|
26
|
+
* Every constraint that changes *which* records match is part of this value and
|
|
27
|
+
* therefore part of resource identity. Pagination is deliberately excluded: it
|
|
28
|
+
* selects a window over an already-determined match set, and the read path
|
|
29
|
+
* answers it from the stored batch's own recorded window instead of pretending a
|
|
30
|
+
* different page was captured.
|
|
31
|
+
*/
|
|
32
|
+
export interface OfflineRegionCanonicalQueryV1 {
|
|
33
|
+
readonly filter: unknown;
|
|
34
|
+
readonly temporalFilter: unknown;
|
|
35
|
+
readonly where: string | null;
|
|
36
|
+
readonly spatialFilter: unknown;
|
|
37
|
+
readonly outFields: readonly string[] | null;
|
|
38
|
+
readonly orderBy: readonly {
|
|
39
|
+
readonly field: string;
|
|
40
|
+
readonly direction: "asc" | "desc";
|
|
41
|
+
}[] | null;
|
|
42
|
+
readonly returnGeometry: boolean | null;
|
|
43
|
+
readonly outSr: string | number | null;
|
|
44
|
+
}
|
|
45
|
+
/** A canonical query plus the pagination window the caller asked for. */
|
|
46
|
+
export interface OfflineRegionQueryScopeV1 {
|
|
47
|
+
readonly canonical: OfflineRegionCanonicalQueryV1;
|
|
48
|
+
readonly pagination: {
|
|
49
|
+
readonly offset: number;
|
|
50
|
+
readonly limit?: number;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Credential-free, protocol-neutral identity of one offline selection. */
|
|
54
|
+
export interface OfflineRegionSelectionIdentityV1 {
|
|
55
|
+
readonly sourceId: string;
|
|
56
|
+
/** Already-normalized credential-free endpoint identity. */
|
|
57
|
+
readonly endpoint: string;
|
|
58
|
+
readonly authorizationScopeDigest: `sha256:${string}`;
|
|
59
|
+
readonly sourceVersion: string;
|
|
60
|
+
readonly schemaVersion: string;
|
|
61
|
+
readonly planVersion: string;
|
|
62
|
+
readonly bounds: OfflineRegionBounds;
|
|
63
|
+
readonly minZoom?: number;
|
|
64
|
+
readonly maxZoom?: number;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Reduce a {@link Query} to its canonical identity plus its pagination window.
|
|
68
|
+
*
|
|
69
|
+
* Fails closed twice over. An unknown member is refused rather than dropped,
|
|
70
|
+
* because dropping it would answer a narrower question than the caller asked;
|
|
71
|
+
* and `aggregation` is refused outright, because a stored feature batch is not
|
|
72
|
+
* an aggregate and computing one locally would return a number the region never
|
|
73
|
+
* observed.
|
|
74
|
+
*/
|
|
75
|
+
export declare function canonicalizeOfflineRegionQuery(query?: Query, sourceId?: string): OfflineRegionQueryScopeV1;
|
|
76
|
+
/** Domain-separated fingerprint of a canonical query. */
|
|
77
|
+
export declare function offlineRegionQueryFingerprint(canonical: OfflineRegionCanonicalQueryV1): Promise<`sha256:${string}`>;
|
|
78
|
+
/**
|
|
79
|
+
* Deterministic, credential-free identity for one resource of one selection.
|
|
80
|
+
*
|
|
81
|
+
* The digest binds the resource kind, its selector, the source identity, the
|
|
82
|
+
* authorization-scope digest, the captured versions, the bounded extent, and the
|
|
83
|
+
* canonical query. Two selections that differ in any of those — including a
|
|
84
|
+
* scope change — can never address the same stored bytes.
|
|
85
|
+
*/
|
|
86
|
+
export declare function offlineRegionResourceId(input: {
|
|
87
|
+
readonly kind: OfflineRegionResourceKind;
|
|
88
|
+
readonly selection: OfflineRegionSelectionIdentityV1;
|
|
89
|
+
readonly queryFingerprint: `sha256:${string}`;
|
|
90
|
+
readonly selector?: OfflineRegionResourceSelector;
|
|
91
|
+
}): Promise<string>;
|
|
92
|
+
/**
|
|
93
|
+
* Digest of the opaque authorization-scope partition input. Never persisted raw.
|
|
94
|
+
*
|
|
95
|
+
* Domain separation and the trimmed pre-image match
|
|
96
|
+
* `createOfflineRegionManifest()` exactly, so a read can compare its caller's
|
|
97
|
+
* current scope against a stored manifest's digest without re-deriving either.
|
|
98
|
+
*/
|
|
99
|
+
export declare function offlineRegionAuthorizationScopeDigest(fingerprint: string): Promise<`sha256:${string}`>;
|
|
100
|
+
/** Highest tile zoom an offline region can address, matching the manifest's own ceiling. */
|
|
101
|
+
export declare const OFFLINE_REGION_MAX_TILE_ZOOM = 30;
|
|
102
|
+
/** Default tile-matrix-set discriminator for a region that names none. */
|
|
103
|
+
export declare const OFFLINE_REGION_DEFAULT_TILE_MATRIX_SET = "default";
|
|
104
|
+
export interface OfflineRegionTileSelectorOptions {
|
|
105
|
+
/** `tms` rows are folded onto their canonical XYZ row before addressing. */
|
|
106
|
+
readonly scheme?: QueryTileScheme;
|
|
107
|
+
/** Separates pyramids that share coordinates but not a grid (for example an OGC tile-matrix set). */
|
|
108
|
+
readonly tileMatrixSetId?: string;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Fold a tile coordinate onto the canonical XYZ identity an offline region stores.
|
|
112
|
+
*
|
|
113
|
+
* This reuses the contract's {@link QueryTileKey} vocabulary but deliberately not
|
|
114
|
+
* `normalizeQueryTileKey()`, whose clamping is right for a renderer and wrong for
|
|
115
|
+
* a content-addressed cache: clamping an out-of-range row would quietly address a
|
|
116
|
+
* *different* tile's bytes. Longitude wrapping is kept, because a wrapped column
|
|
117
|
+
* is the same geographic tile; an out-of-range row or zoom is refused instead.
|
|
118
|
+
*/
|
|
119
|
+
export declare function normalizeOfflineRegionTileKey(input: QueryTileKeyInput, options?: OfflineRegionTileSelectorOptions): QueryTileKey;
|
|
120
|
+
/** Deterministic selector addressing one tile inside a selection. */
|
|
121
|
+
export declare function offlineRegionTileSelector(tile: QueryTileKeyInput, options?: OfflineRegionTileSelectorOptions): OfflineRegionResourceSelector;
|
|
122
|
+
/** Deterministic selector addressing one opaque asset (style, glyph, sprite, attachment). */
|
|
123
|
+
export declare function offlineRegionAssetSelector(key: string): OfflineRegionResourceSelector;
|
|
124
|
+
/** Deterministic selector addressing one metadata document (descriptor, discovery response). */
|
|
125
|
+
export declare function offlineRegionMetadataSelector(document: string): OfflineRegionResourceSelector;
|
|
126
|
+
/** WGS84 lon/lat envelope of a canonical XYZ tile, in the Web Mercator pyramid. */
|
|
127
|
+
export declare function offlineRegionTileEnvelope(tile: QueryTileKey): OfflineRegionBounds;
|
|
128
|
+
/**
|
|
129
|
+
* Whether a declared CRS is provably WGS84 longitude/latitude.
|
|
130
|
+
*
|
|
131
|
+
* Only a recognized identifier returns `true`. An unrecognized CRS makes no
|
|
132
|
+
* claim either way, so callers must not treat `false` as "not WGS84" — they must
|
|
133
|
+
* treat it as "cannot compare geometrically", and fall back to identity alone.
|
|
134
|
+
*/
|
|
135
|
+
export declare function isWgs84LonLatCrs(crs: string): boolean;
|
|
136
|
+
/** How a requested extent relates to a stored region's extent. */
|
|
137
|
+
export type OfflineRegionBoundsRelation = "equal" | "contained" | "outside";
|
|
138
|
+
/** Compare a requested extent to a region extent in the region's own CRS. */
|
|
139
|
+
export declare function compareOfflineRegionBounds(requested: OfflineRegionBounds, region: OfflineRegionBounds): OfflineRegionBoundsRelation;
|
|
140
|
+
//# sourceMappingURL=selection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selection.d.ts","sourceRoot":"","sources":["../../../src/offline/selection.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { HonuaCapabilityNotSupportedError } from "../core/errors.js";
|
|
2
|
+
import { screenPersistedString } from "./credential-screen.js";
|
|
3
|
+
import { canonicalJson, compareCodeUnits, identityDigest, sha256 } from "./digest.js";
|
|
4
|
+
import { HonuaOfflineRegionError } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Protocol-neutral selection identity for offline reads.
|
|
7
|
+
*
|
|
8
|
+
* A persisted region is addressed by what the caller asked for — a source, a
|
|
9
|
+
* canonical query, a bounded extent, an authorization scope, and the source /
|
|
10
|
+
* schema / plan versions it was captured at — never by a request URL. Snapshot
|
|
11
|
+
* planning and the read path both derive resource identity here, so a read can
|
|
12
|
+
* only ever find bytes that were stored for exactly that selection.
|
|
13
|
+
*
|
|
14
|
+
* @experimental
|
|
15
|
+
*/
|
|
16
|
+
/** Protocol name reported by capability failures raised on the offline read path. */
|
|
17
|
+
export const OFFLINE_REGION_PROTOCOL = "offline-region";
|
|
18
|
+
/** Query members the offline read path understands. Anything else fails closed. */
|
|
19
|
+
const SUPPORTED_QUERY_KEYS = new Set([
|
|
20
|
+
"filter",
|
|
21
|
+
"temporalFilter",
|
|
22
|
+
"where",
|
|
23
|
+
"spatialFilter",
|
|
24
|
+
"outFields",
|
|
25
|
+
"orderBy",
|
|
26
|
+
"pagination",
|
|
27
|
+
"aggregation",
|
|
28
|
+
"returnGeometry",
|
|
29
|
+
"outSr",
|
|
30
|
+
"signal",
|
|
31
|
+
]);
|
|
32
|
+
const MAX_SELECTOR_SEGMENTS = 16;
|
|
33
|
+
const MAX_SELECTOR_TEXT_LENGTH = 512;
|
|
34
|
+
const MAX_CANONICAL_QUERY_BYTES = 64 * 1024;
|
|
35
|
+
const MAX_QUERY_DEPTH = 32;
|
|
36
|
+
/**
|
|
37
|
+
* Reduce a {@link Query} to its canonical identity plus its pagination window.
|
|
38
|
+
*
|
|
39
|
+
* Fails closed twice over. An unknown member is refused rather than dropped,
|
|
40
|
+
* because dropping it would answer a narrower question than the caller asked;
|
|
41
|
+
* and `aggregation` is refused outright, because a stored feature batch is not
|
|
42
|
+
* an aggregate and computing one locally would return a number the region never
|
|
43
|
+
* observed.
|
|
44
|
+
*/
|
|
45
|
+
export function canonicalizeOfflineRegionQuery(query, sourceId) {
|
|
46
|
+
if (query === undefined)
|
|
47
|
+
return { canonical: emptyCanonicalQuery(), pagination: { offset: 0 } };
|
|
48
|
+
const record = plainRecord(query, "query");
|
|
49
|
+
for (const key in record) {
|
|
50
|
+
if (Object.hasOwn(record, key) && !SUPPORTED_QUERY_KEYS.has(key)) {
|
|
51
|
+
throw new HonuaCapabilityNotSupportedError(`query.${key}`, OFFLINE_REGION_PROTOCOL, sourceId, {
|
|
52
|
+
context: { reasonCode: "unsupported-query-member" },
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (record.aggregation !== undefined) {
|
|
57
|
+
throw new HonuaCapabilityNotSupportedError("queryAggregate", OFFLINE_REGION_PROTOCOL, sourceId, {
|
|
58
|
+
context: { reasonCode: "aggregation-not-stored" },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const canonical = {
|
|
62
|
+
filter: jsonValue(record.filter, "query.filter"),
|
|
63
|
+
temporalFilter: jsonValue(record.temporalFilter, "query.temporalFilter"),
|
|
64
|
+
where: optionalText(record.where, "query.where"),
|
|
65
|
+
spatialFilter: jsonValue(record.spatialFilter, "query.spatialFilter"),
|
|
66
|
+
outFields: normalizeOutFields(record.outFields),
|
|
67
|
+
orderBy: normalizeOrderBy(record.orderBy),
|
|
68
|
+
returnGeometry: record.returnGeometry === undefined ? null : requireBoolean(record.returnGeometry, "query.returnGeometry"),
|
|
69
|
+
outSr: normalizeOutSr(record.outSr),
|
|
70
|
+
};
|
|
71
|
+
const encoded = canonicalJson(canonical);
|
|
72
|
+
if (encoded.length > MAX_CANONICAL_QUERY_BYTES) {
|
|
73
|
+
throw invalid("query exceeds the canonical offline identity budget.", "query");
|
|
74
|
+
}
|
|
75
|
+
return { canonical, pagination: normalizePagination(record.pagination) };
|
|
76
|
+
}
|
|
77
|
+
/** Domain-separated fingerprint of a canonical query. */
|
|
78
|
+
export function offlineRegionQueryFingerprint(canonical) {
|
|
79
|
+
return identityDigest("honua-offline-query", canonical);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Deterministic, credential-free identity for one resource of one selection.
|
|
83
|
+
*
|
|
84
|
+
* The digest binds the resource kind, its selector, the source identity, the
|
|
85
|
+
* authorization-scope digest, the captured versions, the bounded extent, and the
|
|
86
|
+
* canonical query. Two selections that differ in any of those — including a
|
|
87
|
+
* scope change — can never address the same stored bytes.
|
|
88
|
+
*/
|
|
89
|
+
export async function offlineRegionResourceId(input) {
|
|
90
|
+
const kind = input.kind;
|
|
91
|
+
const digest = await identityDigest("honua-offline-resource", {
|
|
92
|
+
kind,
|
|
93
|
+
selector: normalizeSelector(input.selector),
|
|
94
|
+
query: input.queryFingerprint,
|
|
95
|
+
source: {
|
|
96
|
+
id: input.selection.sourceId,
|
|
97
|
+
endpoint: input.selection.endpoint,
|
|
98
|
+
authorizationScopeDigest: input.selection.authorizationScopeDigest,
|
|
99
|
+
sourceVersion: input.selection.sourceVersion,
|
|
100
|
+
schemaVersion: input.selection.schemaVersion,
|
|
101
|
+
planVersion: input.selection.planVersion,
|
|
102
|
+
},
|
|
103
|
+
bounds: input.selection.bounds,
|
|
104
|
+
minZoom: input.selection.minZoom ?? null,
|
|
105
|
+
maxZoom: input.selection.maxZoom ?? null,
|
|
106
|
+
});
|
|
107
|
+
return `${kind}/${digest.slice("sha256:".length)}`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Digest of the opaque authorization-scope partition input. Never persisted raw.
|
|
111
|
+
*
|
|
112
|
+
* Domain separation and the trimmed pre-image match
|
|
113
|
+
* `createOfflineRegionManifest()` exactly, so a read can compare its caller's
|
|
114
|
+
* current scope against a stored manifest's digest without re-deriving either.
|
|
115
|
+
*/
|
|
116
|
+
export function offlineRegionAuthorizationScopeDigest(fingerprint) {
|
|
117
|
+
if (typeof fingerprint !== "string" || fingerprint.trim().length === 0) {
|
|
118
|
+
throw invalid("authorizationScopeFingerprint must be a non-empty string.", "authorizationScopeFingerprint");
|
|
119
|
+
}
|
|
120
|
+
return sha256(`honua-offline-scope:v1:${fingerprint.trim()}`);
|
|
121
|
+
}
|
|
122
|
+
/** Highest tile zoom an offline region can address, matching the manifest's own ceiling. */
|
|
123
|
+
export const OFFLINE_REGION_MAX_TILE_ZOOM = 30;
|
|
124
|
+
/** Default tile-matrix-set discriminator for a region that names none. */
|
|
125
|
+
export const OFFLINE_REGION_DEFAULT_TILE_MATRIX_SET = "default";
|
|
126
|
+
/**
|
|
127
|
+
* Fold a tile coordinate onto the canonical XYZ identity an offline region stores.
|
|
128
|
+
*
|
|
129
|
+
* This reuses the contract's {@link QueryTileKey} vocabulary but deliberately not
|
|
130
|
+
* `normalizeQueryTileKey()`, whose clamping is right for a renderer and wrong for
|
|
131
|
+
* a content-addressed cache: clamping an out-of-range row would quietly address a
|
|
132
|
+
* *different* tile's bytes. Longitude wrapping is kept, because a wrapped column
|
|
133
|
+
* is the same geographic tile; an out-of-range row or zoom is refused instead.
|
|
134
|
+
*/
|
|
135
|
+
export function normalizeOfflineRegionTileKey(input, options = {}) {
|
|
136
|
+
const scheme = options.scheme ?? "xyz";
|
|
137
|
+
if (scheme !== "xyz" && scheme !== "tms")
|
|
138
|
+
throw invalid("tile scheme must be xyz or tms.", "tile.scheme");
|
|
139
|
+
const z = tileInteger(input.z ?? input.tileMatrix, "tile.z");
|
|
140
|
+
const x = tileInteger(input.x ?? input.tileCol, "tile.x");
|
|
141
|
+
const y = tileInteger(input.y ?? input.tileRow, "tile.y");
|
|
142
|
+
if (z < 0 || z > OFFLINE_REGION_MAX_TILE_ZOOM) {
|
|
143
|
+
throw new HonuaOfflineRegionError("out-of-region", `tile.z must be between 0 and ${OFFLINE_REGION_MAX_TILE_ZOOM}.`, { path: "tile.z" });
|
|
144
|
+
}
|
|
145
|
+
const tileCount = 2 ** z;
|
|
146
|
+
if (y < 0 || y >= tileCount) {
|
|
147
|
+
throw new HonuaOfflineRegionError("out-of-region", `tile.y is outside the zoom-${z} tile pyramid.`, {
|
|
148
|
+
path: "tile.y",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return { z, x: ((x % tileCount) + tileCount) % tileCount, y: scheme === "tms" ? tileCount - 1 - y : y };
|
|
152
|
+
}
|
|
153
|
+
/** Deterministic selector addressing one tile inside a selection. */
|
|
154
|
+
export function offlineRegionTileSelector(tile, options = {}) {
|
|
155
|
+
const key = normalizeOfflineRegionTileKey(tile, options);
|
|
156
|
+
const set = options.tileMatrixSetId ?? OFFLINE_REGION_DEFAULT_TILE_MATRIX_SET;
|
|
157
|
+
return ["tile", boundedIdentity(set, "tileMatrixSetId"), key.z, key.x, key.y];
|
|
158
|
+
}
|
|
159
|
+
/** Deterministic selector addressing one opaque asset (style, glyph, sprite, attachment). */
|
|
160
|
+
export function offlineRegionAssetSelector(key) {
|
|
161
|
+
return ["asset", boundedIdentity(key, "asset")];
|
|
162
|
+
}
|
|
163
|
+
/** Deterministic selector addressing one metadata document (descriptor, discovery response). */
|
|
164
|
+
export function offlineRegionMetadataSelector(document) {
|
|
165
|
+
return ["metadata", boundedIdentity(document, "document")];
|
|
166
|
+
}
|
|
167
|
+
/** WGS84 lon/lat envelope of a canonical XYZ tile, in the Web Mercator pyramid. */
|
|
168
|
+
export function offlineRegionTileEnvelope(tile) {
|
|
169
|
+
const tileCount = 2 ** tile.z;
|
|
170
|
+
return {
|
|
171
|
+
minX: (tile.x / tileCount) * 360 - 180,
|
|
172
|
+
minY: mercatorLatitude((tile.y + 1) / tileCount),
|
|
173
|
+
maxX: ((tile.x + 1) / tileCount) * 360 - 180,
|
|
174
|
+
maxY: mercatorLatitude(tile.y / tileCount),
|
|
175
|
+
crs: "EPSG:4326",
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const WGS84_LON_LAT_CRS = new Set([
|
|
179
|
+
"epsg:4326",
|
|
180
|
+
"crs:84",
|
|
181
|
+
"crs84",
|
|
182
|
+
"ogc:crs84",
|
|
183
|
+
"urn:ogc:def:crs:ogc:1.3:crs84",
|
|
184
|
+
"urn:ogc:def:crs:ogc::crs84",
|
|
185
|
+
"urn:ogc:def:crs:epsg::4326",
|
|
186
|
+
"http://www.opengis.net/def/crs/ogc/1.3/crs84",
|
|
187
|
+
"http://www.opengis.net/def/crs/epsg/0/4326",
|
|
188
|
+
]);
|
|
189
|
+
/**
|
|
190
|
+
* Whether a declared CRS is provably WGS84 longitude/latitude.
|
|
191
|
+
*
|
|
192
|
+
* Only a recognized identifier returns `true`. An unrecognized CRS makes no
|
|
193
|
+
* claim either way, so callers must not treat `false` as "not WGS84" — they must
|
|
194
|
+
* treat it as "cannot compare geometrically", and fall back to identity alone.
|
|
195
|
+
*/
|
|
196
|
+
export function isWgs84LonLatCrs(crs) {
|
|
197
|
+
return typeof crs === "string" && WGS84_LON_LAT_CRS.has(crs.trim().toLowerCase());
|
|
198
|
+
}
|
|
199
|
+
/** Compare a requested extent to a region extent in the region's own CRS. */
|
|
200
|
+
export function compareOfflineRegionBounds(requested, region) {
|
|
201
|
+
if (requested.crs !== region.crs)
|
|
202
|
+
return "outside";
|
|
203
|
+
if (requested.minX === region.minX &&
|
|
204
|
+
requested.minY === region.minY &&
|
|
205
|
+
requested.maxX === region.maxX &&
|
|
206
|
+
requested.maxY === region.maxY) {
|
|
207
|
+
return "equal";
|
|
208
|
+
}
|
|
209
|
+
const contained = requested.minX >= region.minX &&
|
|
210
|
+
requested.minY >= region.minY &&
|
|
211
|
+
requested.maxX <= region.maxX &&
|
|
212
|
+
requested.maxY <= region.maxY;
|
|
213
|
+
return contained ? "contained" : "outside";
|
|
214
|
+
}
|
|
215
|
+
/** Latitude of a Web Mercator row fraction, in degrees. */
|
|
216
|
+
function mercatorLatitude(fraction) {
|
|
217
|
+
return (Math.atan(Math.sinh(Math.PI * (1 - 2 * fraction))) * 180) / Math.PI;
|
|
218
|
+
}
|
|
219
|
+
function tileInteger(value, path) {
|
|
220
|
+
const parsed = typeof value === "string" ? Number(value) : value;
|
|
221
|
+
if (typeof parsed !== "number" || !Number.isSafeInteger(parsed)) {
|
|
222
|
+
throw invalid(`${path} must be an integer.`, path);
|
|
223
|
+
}
|
|
224
|
+
return parsed;
|
|
225
|
+
}
|
|
226
|
+
function boundedIdentity(value, path) {
|
|
227
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_SELECTOR_TEXT_LENGTH) {
|
|
228
|
+
throw invalid(`${path} must be a bounded non-empty string.`, path);
|
|
229
|
+
}
|
|
230
|
+
if (screenPersistedString(value, "identity")) {
|
|
231
|
+
throw invalid(`${path} must not carry credential or request-URL shape.`, path);
|
|
232
|
+
}
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
function normalizeSelector(value) {
|
|
236
|
+
if (value === undefined)
|
|
237
|
+
return null;
|
|
238
|
+
const segments = Array.isArray(value) ? value : [value];
|
|
239
|
+
if (segments.length > MAX_SELECTOR_SEGMENTS)
|
|
240
|
+
throw invalid("selector exceeds its segment limit.", "selector");
|
|
241
|
+
return segments.map((segment, index) => {
|
|
242
|
+
const path = `selector[${index}]`;
|
|
243
|
+
if (typeof segment === "number") {
|
|
244
|
+
if (!Number.isSafeInteger(segment))
|
|
245
|
+
throw invalid(`${path} must be a safe integer.`, path);
|
|
246
|
+
return segment;
|
|
247
|
+
}
|
|
248
|
+
if (typeof segment !== "string" || segment.length === 0 || segment.length > MAX_SELECTOR_TEXT_LENGTH) {
|
|
249
|
+
throw invalid(`${path} must be a bounded non-empty string.`, path);
|
|
250
|
+
}
|
|
251
|
+
if (screenPersistedString(segment, "identity")) {
|
|
252
|
+
throw invalid(`${path} must not carry credential or request-URL shape.`, path);
|
|
253
|
+
}
|
|
254
|
+
return segment;
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
function emptyCanonicalQuery() {
|
|
258
|
+
return {
|
|
259
|
+
filter: null,
|
|
260
|
+
temporalFilter: null,
|
|
261
|
+
where: null,
|
|
262
|
+
spatialFilter: null,
|
|
263
|
+
outFields: null,
|
|
264
|
+
orderBy: null,
|
|
265
|
+
returnGeometry: null,
|
|
266
|
+
outSr: null,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function normalizePagination(value) {
|
|
270
|
+
if (value === undefined)
|
|
271
|
+
return { offset: 0 };
|
|
272
|
+
const record = plainRecord(value, "query.pagination");
|
|
273
|
+
for (const key in record) {
|
|
274
|
+
if (Object.hasOwn(record, key) && key !== "offset" && key !== "limit") {
|
|
275
|
+
throw invalid(`query.pagination.${key} is not supported.`, `query.pagination.${key}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const offset = record.offset === undefined ? 0 : nonNegativeInteger(record.offset, "query.pagination.offset");
|
|
279
|
+
if (record.limit === undefined)
|
|
280
|
+
return { offset };
|
|
281
|
+
return { offset, limit: nonNegativeInteger(record.limit, "query.pagination.limit") };
|
|
282
|
+
}
|
|
283
|
+
function normalizeOutFields(value) {
|
|
284
|
+
if (value === undefined)
|
|
285
|
+
return null;
|
|
286
|
+
if (!Array.isArray(value))
|
|
287
|
+
throw invalid("query.outFields must be an array.", "query.outFields");
|
|
288
|
+
const seen = new Set();
|
|
289
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
290
|
+
const field = value[index];
|
|
291
|
+
if (typeof field !== "string" || field.trim().length === 0) {
|
|
292
|
+
throw invalid(`query.outFields[${index}] must be a non-empty string.`, `query.outFields[${index}]`);
|
|
293
|
+
}
|
|
294
|
+
seen.add(field.trim());
|
|
295
|
+
}
|
|
296
|
+
// Projection order carries no meaning, so a stable order keeps two callers
|
|
297
|
+
// that request the same fields on the same stored resource.
|
|
298
|
+
return [...seen].sort(compareCodeUnits);
|
|
299
|
+
}
|
|
300
|
+
function normalizeOrderBy(value) {
|
|
301
|
+
if (value === undefined)
|
|
302
|
+
return null;
|
|
303
|
+
if (!Array.isArray(value))
|
|
304
|
+
throw invalid("query.orderBy must be an array.", "query.orderBy");
|
|
305
|
+
return value.map((entry, index) => {
|
|
306
|
+
const path = `query.orderBy[${index}]`;
|
|
307
|
+
const record = plainRecord(entry, path);
|
|
308
|
+
for (const key in record) {
|
|
309
|
+
if (Object.hasOwn(record, key) && key !== "field" && key !== "direction") {
|
|
310
|
+
throw invalid(`${path}.${key} is not supported.`, `${path}.${key}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (typeof record.field !== "string" || record.field.trim().length === 0) {
|
|
314
|
+
throw invalid(`${path}.field must be a non-empty string.`, `${path}.field`);
|
|
315
|
+
}
|
|
316
|
+
if (record.direction !== undefined && record.direction !== "asc" && record.direction !== "desc") {
|
|
317
|
+
throw invalid(`${path}.direction must be asc or desc.`, `${path}.direction`);
|
|
318
|
+
}
|
|
319
|
+
return { field: record.field.trim(), direction: record.direction ?? "asc" };
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
function normalizeOutSr(value) {
|
|
323
|
+
if (value === undefined)
|
|
324
|
+
return null;
|
|
325
|
+
if (typeof value === "number") {
|
|
326
|
+
if (!Number.isSafeInteger(value))
|
|
327
|
+
throw invalid("query.outSr must be a safe integer or string.", "query.outSr");
|
|
328
|
+
return value;
|
|
329
|
+
}
|
|
330
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
331
|
+
throw invalid("query.outSr must be a safe integer or non-empty string.", "query.outSr");
|
|
332
|
+
}
|
|
333
|
+
return value.trim();
|
|
334
|
+
}
|
|
335
|
+
function optionalText(value, path) {
|
|
336
|
+
if (value === undefined)
|
|
337
|
+
return null;
|
|
338
|
+
if (typeof value !== "string" || value.length === 0)
|
|
339
|
+
throw invalid(`${path} must be a non-empty string.`, path);
|
|
340
|
+
return value;
|
|
341
|
+
}
|
|
342
|
+
function requireBoolean(value, path) {
|
|
343
|
+
if (typeof value !== "boolean")
|
|
344
|
+
throw invalid(`${path} must be a boolean.`, path);
|
|
345
|
+
return value;
|
|
346
|
+
}
|
|
347
|
+
function nonNegativeInteger(value, path) {
|
|
348
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
349
|
+
throw invalid(`${path} must be a non-negative safe integer.`, path);
|
|
350
|
+
}
|
|
351
|
+
return value;
|
|
352
|
+
}
|
|
353
|
+
/** Detach an untrusted query member into plain JSON, refusing anything else. */
|
|
354
|
+
function jsonValue(value, path, depth = 0) {
|
|
355
|
+
if (value === undefined)
|
|
356
|
+
return null;
|
|
357
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
358
|
+
return value;
|
|
359
|
+
if (typeof value === "number") {
|
|
360
|
+
if (!Number.isFinite(value))
|
|
361
|
+
throw invalid(`${path} must be finite.`, path);
|
|
362
|
+
return Object.is(value, -0) ? 0 : value;
|
|
363
|
+
}
|
|
364
|
+
if (depth >= MAX_QUERY_DEPTH)
|
|
365
|
+
throw invalid(`${path} exceeds the canonical nesting limit.`, path);
|
|
366
|
+
if (Array.isArray(value))
|
|
367
|
+
return value.map((entry, index) => jsonValue(entry, `${path}[${index}]`, depth + 1));
|
|
368
|
+
const record = plainRecord(value, path);
|
|
369
|
+
const out = {};
|
|
370
|
+
for (const key in record) {
|
|
371
|
+
if (!Object.hasOwn(record, key))
|
|
372
|
+
continue;
|
|
373
|
+
const child = record[key];
|
|
374
|
+
if (child === undefined)
|
|
375
|
+
continue;
|
|
376
|
+
out[key] = jsonValue(child, `${path}.${key}`, depth + 1);
|
|
377
|
+
}
|
|
378
|
+
return out;
|
|
379
|
+
}
|
|
380
|
+
function plainRecord(value, path) {
|
|
381
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
382
|
+
throw invalid(`${path} must be a plain object.`, path);
|
|
383
|
+
}
|
|
384
|
+
const prototype = Object.getPrototypeOf(value);
|
|
385
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
386
|
+
throw invalid(`${path} must be a plain object.`, path);
|
|
387
|
+
return value;
|
|
388
|
+
}
|
|
389
|
+
function invalid(message, path) {
|
|
390
|
+
return new HonuaOfflineRegionError("invalid-manifest", message, { path });
|
|
391
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { Query, Result } from "../contract/types.js";
|
|
2
|
+
import type { HonuaFieldInfo, HonuaTypedFeature } from "../core/types.js";
|
|
3
|
+
import { type OfflineRegionCanonicalQueryV1, type OfflineRegionResourceSelector, type OfflineRegionSelectionIdentityV1 } from "./selection.js";
|
|
4
|
+
import { type OfflineRegionBounds, type OfflineRegionLimits, type OfflineRegionManifestV1, type OfflineRegionObservation, type OfflineRegionResourceKind, type OfflineRegionResourceLoader, type OfflineRegionValidator } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Snapshot planning: turn a protocol-neutral selection into a downloadable region.
|
|
7
|
+
*
|
|
8
|
+
* The planner is the missing producer between the contract and the region store.
|
|
9
|
+
* It takes a source identity, a canonical {@link Query}, a bounded extent, and the
|
|
10
|
+
* payloads an application already holds, and emits a manifest whose resource
|
|
11
|
+
* identities are deterministic functions of those inputs — never of a signed or
|
|
12
|
+
* token-bearing request URL. The same identity function answers the later read,
|
|
13
|
+
* so a snapshot and a read agree without persisting any addressing state.
|
|
14
|
+
*
|
|
15
|
+
* @experimental
|
|
16
|
+
*/
|
|
17
|
+
export declare const HONUA_OFFLINE_REGION_SNAPSHOT_KIND: "honua.offline-region-snapshot";
|
|
18
|
+
export declare const HONUA_OFFLINE_REGION_SNAPSHOT_VERSION: "1.0";
|
|
19
|
+
export declare const HONUA_OFFLINE_FEATURE_BATCH_KIND: "honua.offline-feature-batch";
|
|
20
|
+
export declare const HONUA_OFFLINE_FEATURE_BATCH_VERSION: "1.0";
|
|
21
|
+
/** Default ceiling on how many resources one planned snapshot may contain. */
|
|
22
|
+
export declare const DEFAULT_OFFLINE_REGION_SNAPSHOT_MAX_CONTENTS = 10000;
|
|
23
|
+
/**
|
|
24
|
+
* One stored answer to a canonical query.
|
|
25
|
+
*
|
|
26
|
+
* The envelope records the window it covers and whether more records existed, so
|
|
27
|
+
* a later read can tell the difference between "this is everything" and "this is
|
|
28
|
+
* the first page" without guessing.
|
|
29
|
+
*/
|
|
30
|
+
export interface OfflineRegionFeatureBatchV1<T = Record<string, unknown>> {
|
|
31
|
+
readonly kind: typeof HONUA_OFFLINE_FEATURE_BATCH_KIND;
|
|
32
|
+
readonly version: typeof HONUA_OFFLINE_FEATURE_BATCH_VERSION;
|
|
33
|
+
/** Window this batch covers, in the canonical query's own pagination terms. */
|
|
34
|
+
readonly pagination: {
|
|
35
|
+
readonly offset: number;
|
|
36
|
+
readonly limit?: number;
|
|
37
|
+
};
|
|
38
|
+
/** True when the source signalled that more records matched than were captured. */
|
|
39
|
+
readonly exceededTransferLimit: boolean;
|
|
40
|
+
readonly features: readonly HonuaTypedFeature<T>[];
|
|
41
|
+
readonly totalCount?: number;
|
|
42
|
+
readonly fields?: readonly HonuaFieldInfo[];
|
|
43
|
+
}
|
|
44
|
+
/** One payload contributed to a planned snapshot. */
|
|
45
|
+
export interface OfflineRegionSnapshotContentV1 {
|
|
46
|
+
readonly kind: OfflineRegionResourceKind;
|
|
47
|
+
/** Exact bytes to persist. Tiles and assets stay opaque; nothing is re-encoded. */
|
|
48
|
+
readonly bytes: Uint8Array | ArrayBuffer;
|
|
49
|
+
readonly contentType?: string;
|
|
50
|
+
/** Discriminator when one selection stores several resources of one kind. */
|
|
51
|
+
readonly selector?: OfflineRegionResourceSelector;
|
|
52
|
+
readonly attributionIds?: readonly string[];
|
|
53
|
+
}
|
|
54
|
+
/** A source, a query, and a bounded extent, plus the payloads they require. */
|
|
55
|
+
export interface OfflineRegionSnapshotSelectionV1 {
|
|
56
|
+
readonly name: string;
|
|
57
|
+
readonly sourceId: string;
|
|
58
|
+
/** Credentials and signed parameters are removed before anything is persisted. */
|
|
59
|
+
readonly endpoint: string | URL;
|
|
60
|
+
/** Opaque ACL/auth partition input. Only its digest is persisted or compared. */
|
|
61
|
+
readonly authorizationScopeFingerprint: string;
|
|
62
|
+
readonly bounds: OfflineRegionBounds;
|
|
63
|
+
readonly minZoom?: number;
|
|
64
|
+
readonly maxZoom?: number;
|
|
65
|
+
readonly sourceVersion: string;
|
|
66
|
+
readonly schemaVersion: string;
|
|
67
|
+
readonly planVersion: string;
|
|
68
|
+
readonly observation: OfflineRegionObservation;
|
|
69
|
+
readonly validator?: OfflineRegionValidator;
|
|
70
|
+
readonly expiresAt?: string;
|
|
71
|
+
readonly attribution?: Readonly<Record<string, string>>;
|
|
72
|
+
/** Protocol-neutral query this snapshot answers. Omitted means "no constraint". */
|
|
73
|
+
readonly query?: Query;
|
|
74
|
+
readonly contents: readonly OfflineRegionSnapshotContentV1[];
|
|
75
|
+
readonly limits?: OfflineRegionLimits & {
|
|
76
|
+
readonly maxContents?: number;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** One planned resource: its deterministic identity and its verified payload size. */
|
|
80
|
+
export interface OfflineRegionSnapshotEntryV1 {
|
|
81
|
+
readonly id: string;
|
|
82
|
+
readonly kind: OfflineRegionResourceKind;
|
|
83
|
+
readonly byteLength: number;
|
|
84
|
+
readonly integrity: `sha256:${string}`;
|
|
85
|
+
readonly contentType?: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A planned snapshot: the manifest to download plus the payloads it names.
|
|
89
|
+
*
|
|
90
|
+
* `payloads` is deliberately not part of the persisted contract — the manifest is
|
|
91
|
+
* the durable artifact. It exists so {@link createOfflineRegionSnapshotLoader}
|
|
92
|
+
* can serve the exact bytes the identities were computed from.
|
|
93
|
+
*/
|
|
94
|
+
export interface OfflineRegionSnapshotV1 {
|
|
95
|
+
readonly kind: typeof HONUA_OFFLINE_REGION_SNAPSHOT_KIND;
|
|
96
|
+
readonly version: typeof HONUA_OFFLINE_REGION_SNAPSHOT_VERSION;
|
|
97
|
+
readonly manifest: OfflineRegionManifestV1;
|
|
98
|
+
readonly selection: OfflineRegionSelectionIdentityV1;
|
|
99
|
+
readonly query: OfflineRegionCanonicalQueryV1;
|
|
100
|
+
readonly queryFingerprint: `sha256:${string}`;
|
|
101
|
+
readonly entries: readonly OfflineRegionSnapshotEntryV1[];
|
|
102
|
+
readonly payloads: ReadonlyMap<string, Uint8Array>;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Plan one bounded snapshot of a source, a query, and an extent.
|
|
106
|
+
*
|
|
107
|
+
* Every resource identity is derived from the selection — source identity,
|
|
108
|
+
* authorization-scope digest, captured versions, extent, canonical query, and the
|
|
109
|
+
* resource's own kind and selector. Identities are therefore stable across runs
|
|
110
|
+
* and processes, contain no request URL, and cannot collide across authorization
|
|
111
|
+
* scopes.
|
|
112
|
+
*/
|
|
113
|
+
export declare function planOfflineRegionSnapshot(selection: OfflineRegionSnapshotSelectionV1): Promise<OfflineRegionSnapshotV1>;
|
|
114
|
+
/**
|
|
115
|
+
* Serve a planned snapshot's payloads to {@link downloadOfflineRegion}.
|
|
116
|
+
*
|
|
117
|
+
* The loader is offline by construction: it hands back the exact bytes the
|
|
118
|
+
* identities were computed from and refuses anything the plan does not name.
|
|
119
|
+
*/
|
|
120
|
+
export declare function createOfflineRegionSnapshotLoader(snapshot: OfflineRegionSnapshotV1): OfflineRegionResourceLoader;
|
|
121
|
+
/** Capture a `Result` as the durable batch envelope a later read answers from. */
|
|
122
|
+
export declare function createOfflineRegionFeatureBatch<T>(result: Pick<Result<T>, "features" | "exceededTransferLimit" | "totalCount" | "fields">, options?: {
|
|
123
|
+
readonly pagination?: {
|
|
124
|
+
readonly offset?: number;
|
|
125
|
+
readonly limit?: number;
|
|
126
|
+
};
|
|
127
|
+
}): OfflineRegionFeatureBatchV1<T>;
|
|
128
|
+
/** Encode a batch as the deterministic UTF-8 bytes the manifest pins. */
|
|
129
|
+
export declare function encodeOfflineRegionFeatureBatch<T>(batch: OfflineRegionFeatureBatchV1<T>): Uint8Array;
|
|
130
|
+
/** Decode and validate stored batch bytes. Anything unexpected fails closed. */
|
|
131
|
+
export declare function decodeOfflineRegionFeatureBatch<T = Record<string, unknown>>(bytes: Uint8Array): OfflineRegionFeatureBatchV1<T>;
|
|
132
|
+
//# sourceMappingURL=snapshot.d.ts.map
|