@honua/app-platform 0.1.12 → 0.1.13
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/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 +10 -6
- 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/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,233 @@
|
|
|
1
|
+
import { sha256 } from "./digest.js";
|
|
2
|
+
import { createOfflineRegionManifest, downloadOfflineRegion, planOfflineRegionAdmission } from "./region.js";
|
|
3
|
+
import { HonuaOfflineRegionError, } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* One conformance suite every offline region store must pass.
|
|
6
|
+
*
|
|
7
|
+
* The IndexedDB adapter can only run in a browser and the in-memory adapter can
|
|
8
|
+
* run anywhere, so the suite is written as plain async functions rather than
|
|
9
|
+
* against a test framework: vitest runs it over the memory store, and the browser
|
|
10
|
+
* evidence runs the identical cases over real IndexedDB. A store that passes here
|
|
11
|
+
* is substitutable in the download coordinator and the read path.
|
|
12
|
+
*
|
|
13
|
+
* @experimental
|
|
14
|
+
*/
|
|
15
|
+
export const HONUA_OFFLINE_REGION_STORE_CONFORMANCE_KIND = "honua.offline-region-store-conformance";
|
|
16
|
+
export const HONUA_OFFLINE_REGION_STORE_CONFORMANCE_VERSION = "1.0";
|
|
17
|
+
const encoder = new TextEncoder();
|
|
18
|
+
const QUOTA = 1024 * 1024;
|
|
19
|
+
/** Names of every case in the suite, in execution order. */
|
|
20
|
+
export const OFFLINE_REGION_STORE_CONFORMANCE_CASES = [
|
|
21
|
+
"empty-store-reports-no-regions",
|
|
22
|
+
"commit-publishes-region-and-resources",
|
|
23
|
+
"read-returns-caller-owned-bytes",
|
|
24
|
+
"absent-resource-reads-undefined",
|
|
25
|
+
"commit-advances-inventory-revision",
|
|
26
|
+
"stale-inventory-commit-is-refused",
|
|
27
|
+
"rollback-discards-uncommitted-writes",
|
|
28
|
+
"admission-evicts-to-fit-quota",
|
|
29
|
+
"credential-bearing-identity-is-refused",
|
|
30
|
+
"byte-length-mismatch-is-refused",
|
|
31
|
+
];
|
|
32
|
+
const CASES = {
|
|
33
|
+
"empty-store-reports-no-regions": async (store) => {
|
|
34
|
+
const inventory = await store.inventory();
|
|
35
|
+
assert(typeof inventory.revision === "string" && inventory.revision.length > 0, "revision must be a non-empty string");
|
|
36
|
+
assert(inventory.regions.length === 0, "a fresh store must report no regions");
|
|
37
|
+
assert((await store.readResource("missing-region", "missing-resource")) === undefined, "absent region must read undefined");
|
|
38
|
+
},
|
|
39
|
+
"commit-publishes-region-and-resources": async (store) => {
|
|
40
|
+
const fixture = await downloadFixture(store, "alpha");
|
|
41
|
+
const inventory = await store.inventory();
|
|
42
|
+
assert(inventory.regions.length === 1, "one committed region must appear in the inventory");
|
|
43
|
+
assert(inventory.regions[0]?.id === fixture.manifest.id, "the inventory must key the region by its manifest id");
|
|
44
|
+
assert(inventory.regions[0]?.logicalByteLength === fixture.manifest.totalLogicalBytes, "the inventory must charge the manifest's logical bytes");
|
|
45
|
+
const read = await store.readResource(fixture.manifest.id, fixture.resourceId);
|
|
46
|
+
assert(read !== undefined, "a committed resource must be readable");
|
|
47
|
+
assert(decode(read.bytes) === fixture.payload, "a committed resource must read back its exact bytes");
|
|
48
|
+
assert(read.manifest.id === fixture.manifest.id, "a read must carry the region's manifest");
|
|
49
|
+
assert(read.resource.id === fixture.resourceId, "a read must carry the resource descriptor");
|
|
50
|
+
},
|
|
51
|
+
"read-returns-caller-owned-bytes": async (store) => {
|
|
52
|
+
const fixture = await downloadFixture(store, "alpha");
|
|
53
|
+
const first = await store.readResource(fixture.manifest.id, fixture.resourceId);
|
|
54
|
+
assert(first !== undefined, "a committed resource must be readable");
|
|
55
|
+
first.bytes[0] = 0;
|
|
56
|
+
const second = await store.readResource(fixture.manifest.id, fixture.resourceId);
|
|
57
|
+
assert(second !== undefined, "a committed resource must stay readable");
|
|
58
|
+
assert(decode(second.bytes) === fixture.payload, "mutating returned bytes must not mutate the store");
|
|
59
|
+
},
|
|
60
|
+
"absent-resource-reads-undefined": async (store) => {
|
|
61
|
+
const fixture = await downloadFixture(store, "alpha");
|
|
62
|
+
assert((await store.readResource(fixture.manifest.id, "features/absent")) === undefined, "an unknown resource id must read undefined");
|
|
63
|
+
assert((await store.readResource("sha256:0", fixture.resourceId)) === undefined, "an unknown region must read undefined");
|
|
64
|
+
},
|
|
65
|
+
"commit-advances-inventory-revision": async (store) => {
|
|
66
|
+
const before = (await store.inventory()).revision;
|
|
67
|
+
await downloadFixture(store, "alpha");
|
|
68
|
+
const after = (await store.inventory()).revision;
|
|
69
|
+
assert(before !== after, "a committed mutation must advance the inventory revision");
|
|
70
|
+
},
|
|
71
|
+
"stale-inventory-commit-is-refused": async (store) => {
|
|
72
|
+
const stale = await store.inventory();
|
|
73
|
+
const first = await downloadFixture(store, "alpha");
|
|
74
|
+
const second = await fixtureManifest("beta");
|
|
75
|
+
const transaction = await store.beginWrite(second.manifest.id);
|
|
76
|
+
await transaction.write(second.manifest.resources[0], encoder.encode(second.payload));
|
|
77
|
+
const outcome = await transaction.commit(second.manifest, receiptFor(second.manifest), {
|
|
78
|
+
expectedInventoryRevision: stale.revision,
|
|
79
|
+
logicalQuotaBytes: QUOTA,
|
|
80
|
+
admission: planOfflineRegionAdmission(second.manifest, stale, { logicalQuotaBytes: QUOTA }),
|
|
81
|
+
});
|
|
82
|
+
assert(outcome === "inventory-changed", "a commit against a stale inventory revision must be refused");
|
|
83
|
+
await transaction.rollback();
|
|
84
|
+
const inventory = await store.inventory();
|
|
85
|
+
assert(inventory.regions.length === 1, "a refused commit must not publish a region");
|
|
86
|
+
assert(inventory.regions[0]?.id === first.manifest.id, "a refused commit must leave the prior region intact");
|
|
87
|
+
},
|
|
88
|
+
"rollback-discards-uncommitted-writes": async (store) => {
|
|
89
|
+
const fixture = await fixtureManifest("alpha");
|
|
90
|
+
const transaction = await store.beginWrite(fixture.manifest.id);
|
|
91
|
+
await transaction.write(fixture.manifest.resources[0], encoder.encode(fixture.payload));
|
|
92
|
+
await transaction.rollback();
|
|
93
|
+
const inventory = await store.inventory();
|
|
94
|
+
assert(inventory.regions.length === 0, "a rolled back transaction must publish nothing");
|
|
95
|
+
assert((await store.readResource(fixture.manifest.id, fixture.resourceId)) === undefined, "a rolled back resource must not be readable");
|
|
96
|
+
},
|
|
97
|
+
"admission-evicts-to-fit-quota": async (store) => {
|
|
98
|
+
const first = await downloadFixture(store, "alpha");
|
|
99
|
+
const second = await fixtureManifest("beta");
|
|
100
|
+
// A quota that fits exactly one region forces the older one out.
|
|
101
|
+
await downloadOfflineRegion(second.manifest, {
|
|
102
|
+
store,
|
|
103
|
+
load: async () => encoder.encode(second.payload),
|
|
104
|
+
logicalQuotaBytes: second.manifest.totalLogicalBytes,
|
|
105
|
+
});
|
|
106
|
+
const inventory = await store.inventory();
|
|
107
|
+
assert(inventory.regions.length === 1, "eviction must leave exactly the admitted region");
|
|
108
|
+
assert(inventory.regions[0]?.id === second.manifest.id, "eviction must retain the newly admitted region");
|
|
109
|
+
assert((await store.readResource(first.manifest.id, first.resourceId)) === undefined, "eviction must remove the evicted region's resources");
|
|
110
|
+
},
|
|
111
|
+
"credential-bearing-identity-is-refused": async (store) => {
|
|
112
|
+
await expectRejection(() => store.beginWrite("https://user:secret@example.test/region?token=abc"), "a credential-bearing region id must be refused");
|
|
113
|
+
},
|
|
114
|
+
"byte-length-mismatch-is-refused": async (store) => {
|
|
115
|
+
const fixture = await fixtureManifest("alpha");
|
|
116
|
+
const transaction = await store.beginWrite(fixture.manifest.id);
|
|
117
|
+
await expectRejection(() => transaction.write(fixture.manifest.resources[0], encoder.encode(`${fixture.payload}-extra`)), "a byte length that disagrees with the descriptor must be refused");
|
|
118
|
+
await transaction.rollback();
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Run the suite and report per-case outcomes.
|
|
123
|
+
*
|
|
124
|
+
* Cases never share a store: each one is handed a fresh store from
|
|
125
|
+
* `createStore`, so a failure cannot cascade into a false failure downstream.
|
|
126
|
+
*/
|
|
127
|
+
export async function runOfflineRegionStoreConformance(options) {
|
|
128
|
+
const selected = options.only ?? OFFLINE_REGION_STORE_CONFORMANCE_CASES;
|
|
129
|
+
const cases = [];
|
|
130
|
+
for (const name of selected) {
|
|
131
|
+
const body = CASES[name];
|
|
132
|
+
if (!body) {
|
|
133
|
+
cases.push({ name, status: "failed", detail: "unknown conformance case" });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
let store;
|
|
137
|
+
try {
|
|
138
|
+
store = await options.createStore();
|
|
139
|
+
await body(store);
|
|
140
|
+
cases.push({ name, status: "passed" });
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
cases.push({ name, status: "failed", detail: describe(error) });
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
if (store && options.disposeStore) {
|
|
147
|
+
try {
|
|
148
|
+
await options.disposeStore(store);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Teardown failures must not rewrite a case's own verdict.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const failed = cases.filter((entry) => entry.status === "failed").length;
|
|
157
|
+
return {
|
|
158
|
+
kind: HONUA_OFFLINE_REGION_STORE_CONFORMANCE_KIND,
|
|
159
|
+
version: HONUA_OFFLINE_REGION_STORE_CONFORMANCE_VERSION,
|
|
160
|
+
...(options.label ? { label: options.label } : {}),
|
|
161
|
+
total: cases.length,
|
|
162
|
+
passed: cases.length - failed,
|
|
163
|
+
failed,
|
|
164
|
+
cases,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
async function fixtureManifest(name) {
|
|
168
|
+
const payload = `{"region":"${name}"}`;
|
|
169
|
+
const resourceId = `features/${name}`;
|
|
170
|
+
const resource = {
|
|
171
|
+
id: resourceId,
|
|
172
|
+
kind: "features",
|
|
173
|
+
byteLength: encoder.encode(payload).byteLength,
|
|
174
|
+
integrity: await sha256(encoder.encode(payload)),
|
|
175
|
+
contentType: "application/json",
|
|
176
|
+
};
|
|
177
|
+
const manifest = await createOfflineRegionManifest({
|
|
178
|
+
name: `Conformance region ${name}`,
|
|
179
|
+
sourceId: "conformance-source",
|
|
180
|
+
endpoint: "https://example.test/FeatureServer/0",
|
|
181
|
+
authorizationScopeFingerprint: `conformance-scope-${name}`,
|
|
182
|
+
bounds: { minX: -1, minY: -1, maxX: 1, maxY: 1, crs: "EPSG:4326" },
|
|
183
|
+
sourceVersion: "source-v1",
|
|
184
|
+
schemaVersion: "schema-v1",
|
|
185
|
+
planVersion: "plan-v1",
|
|
186
|
+
observation: { state: "live", observedAt: "2026-01-01T00:00:00.000Z" },
|
|
187
|
+
attribution: { fixture: "Honua conformance fixture" },
|
|
188
|
+
resources: [resource],
|
|
189
|
+
});
|
|
190
|
+
return { manifest, resourceId, payload };
|
|
191
|
+
}
|
|
192
|
+
async function downloadFixture(store, name) {
|
|
193
|
+
const fixture = await fixtureManifest(name);
|
|
194
|
+
await downloadOfflineRegion(fixture.manifest, {
|
|
195
|
+
store,
|
|
196
|
+
load: async () => encoder.encode(fixture.payload),
|
|
197
|
+
logicalQuotaBytes: QUOTA,
|
|
198
|
+
});
|
|
199
|
+
return fixture;
|
|
200
|
+
}
|
|
201
|
+
function receiptFor(manifest) {
|
|
202
|
+
return {
|
|
203
|
+
regionId: manifest.id,
|
|
204
|
+
resourceCount: manifest.resources.length,
|
|
205
|
+
logicalByteLength: manifest.totalLogicalBytes,
|
|
206
|
+
evictedRegionIds: [],
|
|
207
|
+
integrity: "verified",
|
|
208
|
+
quotaAccounting: "logical-payload-bytes",
|
|
209
|
+
completedAt: "2026-01-01T00:00:00.000Z",
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
async function expectRejection(body, message) {
|
|
213
|
+
try {
|
|
214
|
+
await body();
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
assert(error instanceof Error, `${message} (a typed error is required)`);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
throw new Error(message);
|
|
221
|
+
}
|
|
222
|
+
function assert(condition, message) {
|
|
223
|
+
if (!condition)
|
|
224
|
+
throw new Error(message);
|
|
225
|
+
}
|
|
226
|
+
function decode(bytes) {
|
|
227
|
+
return new TextDecoder().decode(bytes);
|
|
228
|
+
}
|
|
229
|
+
function describe(error) {
|
|
230
|
+
if (error instanceof HonuaOfflineRegionError)
|
|
231
|
+
return `${error.code}: ${error.message}`;
|
|
232
|
+
return error instanceof Error ? error.message : String(error);
|
|
233
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type OfflineRegionTileReadV1, type ReadOfflineRegionTileOptions } from "./read-resource.js";
|
|
2
|
+
import { type OfflineRegionManifestV1 } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Binding persisted tiles to the map runtime's existing protocol seam.
|
|
5
|
+
*
|
|
6
|
+
* The SDK already registers custom URL protocols with MapLibre through a
|
|
7
|
+
* `addProtocol(scheme, handler)` registrar (`src/runtime/pmtiles-protocol.ts`),
|
|
8
|
+
* so offline tiles reuse that seam rather than introducing a second tile
|
|
9
|
+
* pipeline: a style names `offline-region://…/{z}/{x}/{y}` tiles, and the handler
|
|
10
|
+
* answers each one from the persisted region under the full read discipline. A
|
|
11
|
+
* tile the region does not hold rejects with its typed reason — nothing falls
|
|
12
|
+
* back to the network, and nothing renders bytes the region never stored.
|
|
13
|
+
*
|
|
14
|
+
* Registration stays application-owned. The runtime does not yet auto-register
|
|
15
|
+
* this protocol or rewrite a style's tile URLs onto it.
|
|
16
|
+
*
|
|
17
|
+
* @experimental
|
|
18
|
+
*/
|
|
19
|
+
/** URL scheme a style uses to address tiles inside a persisted region. */
|
|
20
|
+
export declare const OFFLINE_REGION_TILE_SCHEME: "offline-region";
|
|
21
|
+
/** One parsed `offline-region://` tile address. */
|
|
22
|
+
export interface OfflineRegionTileAddressV1 {
|
|
23
|
+
readonly tileMatrixSetId: string;
|
|
24
|
+
readonly z: number;
|
|
25
|
+
readonly x: number;
|
|
26
|
+
readonly y: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Request shape MapLibre hands a protocol handler.
|
|
30
|
+
*
|
|
31
|
+
* Declared structurally so `@honua/sdk-js/offline` never imports the map runtime
|
|
32
|
+
* or the `maplibre-gl` peer; it is the same shape the runtime's own registrar
|
|
33
|
+
* already passes.
|
|
34
|
+
*/
|
|
35
|
+
export interface OfflineRegionProtocolRequest {
|
|
36
|
+
readonly url: string;
|
|
37
|
+
}
|
|
38
|
+
export interface OfflineRegionProtocolResponse {
|
|
39
|
+
readonly data: ArrayBuffer;
|
|
40
|
+
readonly cacheControl?: string;
|
|
41
|
+
readonly expires?: string;
|
|
42
|
+
}
|
|
43
|
+
export type OfflineRegionProtocolHandler = (request: OfflineRegionProtocolRequest, abortController?: AbortController) => Promise<OfflineRegionProtocolResponse>;
|
|
44
|
+
/** The `addProtocol`/`removeProtocol` pair the map runtime already speaks. */
|
|
45
|
+
export interface OfflineRegionProtocolRegistrar {
|
|
46
|
+
addProtocol(scheme: string, handler: OfflineRegionProtocolHandler): void;
|
|
47
|
+
removeProtocol?(scheme: string): void;
|
|
48
|
+
}
|
|
49
|
+
export interface CreateOfflineRegionTileProtocolOptions extends Omit<ReadOfflineRegionTileOptions, "tile" | "scheme" | "tileMatrixSetId" | "signal"> {
|
|
50
|
+
readonly manifest: OfflineRegionManifestV1;
|
|
51
|
+
/** Observe every tile served, for diagnostics. Exceptions are ignored. */
|
|
52
|
+
readonly onTile?: (read: OfflineRegionTileReadV1) => void;
|
|
53
|
+
}
|
|
54
|
+
/** Build the tile-URL template a MapLibre source uses to address a region. */
|
|
55
|
+
export declare function buildOfflineRegionTileUrlTemplate(options?: {
|
|
56
|
+
readonly tileMatrixSetId?: string;
|
|
57
|
+
}): string;
|
|
58
|
+
/**
|
|
59
|
+
* Parse an `offline-region://` tile URL.
|
|
60
|
+
*
|
|
61
|
+
* Parsing is a bounded segment split with no regular expression, and a URL
|
|
62
|
+
* carrying a query or fragment is refused outright: a tile address is an
|
|
63
|
+
* identity, and anything that could smuggle a token into one is not a tile
|
|
64
|
+
* address.
|
|
65
|
+
*/
|
|
66
|
+
export declare function parseOfflineRegionTileUrl(url: string): OfflineRegionTileAddressV1;
|
|
67
|
+
/**
|
|
68
|
+
* Create a MapLibre protocol handler that serves tiles from a persisted region.
|
|
69
|
+
*
|
|
70
|
+
* Register it with the same registrar the runtime uses for other custom schemes:
|
|
71
|
+
* `registrar.addProtocol(OFFLINE_REGION_TILE_SCHEME, handler)`.
|
|
72
|
+
*/
|
|
73
|
+
export declare function createOfflineRegionTileProtocol(options: CreateOfflineRegionTileProtocolOptions): OfflineRegionProtocolHandler;
|
|
74
|
+
/**
|
|
75
|
+
* Register the offline tile protocol and return its disposer.
|
|
76
|
+
*
|
|
77
|
+
* The registrar is the application's — usually `maplibre-gl` itself — so the SDK
|
|
78
|
+
* neither imports the peer nor decides when a map should read from a region.
|
|
79
|
+
*/
|
|
80
|
+
export declare function registerOfflineRegionTileProtocol(registrar: OfflineRegionProtocolRegistrar, options: CreateOfflineRegionTileProtocolOptions): () => void;
|
|
81
|
+
//# sourceMappingURL=tile-protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tile-protocol.d.ts","sourceRoot":"","sources":["../../../src/offline/tile-protocol.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,4BAA4B,EAElC,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAA2B,KAAK,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAEnF;;;;;;;;;;;;;;;GAeG;AAEH,0EAA0E;AAC1E,eAAO,MAAM,0BAA0B,kBAA0B,CAAC;AAIlE,mDAAmD;AACnD,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,MAAM,4BAA4B,GAAG,CACzC,OAAO,EAAE,4BAA4B,EACrC,eAAe,CAAC,EAAE,eAAe,KAC9B,OAAO,CAAC,6BAA6B,CAAC,CAAC;AAE5C,8EAA8E;AAC9E,MAAM,WAAW,8BAA8B;IAC7C,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,4BAA4B,GAAG,IAAI,CAAC;IACzE,cAAc,CAAC,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,sCACf,SAAQ,IAAI,CAAC,4BAA4B,EAAE,MAAM,GAAG,QAAQ,GAAG,iBAAiB,GAAG,QAAQ,CAAC;IAC5F,QAAQ,CAAC,QAAQ,EAAE,uBAAuB,CAAC;IAC3C,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,uBAAuB,KAAK,IAAI,CAAC;CAC3D;AAED,8EAA8E;AAC9E,wBAAgB,iCAAiC,CAAC,OAAO,GAAE;IAAE,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,MAAM,CAQ7G;AAED;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,0BAA0B,CAejF;AAED;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,sCAAsC,GAC9C,4BAA4B,CAwB9B;AAED;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAC/C,SAAS,EAAE,8BAA8B,EACzC,OAAO,EAAE,sCAAsC,GAC9C,MAAM,IAAI,CAMZ"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { readOfflineRegionTile, } from "./read-resource.js";
|
|
2
|
+
import { OFFLINE_REGION_DEFAULT_TILE_MATRIX_SET, OFFLINE_REGION_PROTOCOL } from "./selection.js";
|
|
3
|
+
import { HonuaOfflineRegionError } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Binding persisted tiles to the map runtime's existing protocol seam.
|
|
6
|
+
*
|
|
7
|
+
* The SDK already registers custom URL protocols with MapLibre through a
|
|
8
|
+
* `addProtocol(scheme, handler)` registrar (`src/runtime/pmtiles-protocol.ts`),
|
|
9
|
+
* so offline tiles reuse that seam rather than introducing a second tile
|
|
10
|
+
* pipeline: a style names `offline-region://…/{z}/{x}/{y}` tiles, and the handler
|
|
11
|
+
* answers each one from the persisted region under the full read discipline. A
|
|
12
|
+
* tile the region does not hold rejects with its typed reason — nothing falls
|
|
13
|
+
* back to the network, and nothing renders bytes the region never stored.
|
|
14
|
+
*
|
|
15
|
+
* Registration stays application-owned. The runtime does not yet auto-register
|
|
16
|
+
* this protocol or rewrite a style's tile URLs onto it.
|
|
17
|
+
*
|
|
18
|
+
* @experimental
|
|
19
|
+
*/
|
|
20
|
+
/** URL scheme a style uses to address tiles inside a persisted region. */
|
|
21
|
+
export const OFFLINE_REGION_TILE_SCHEME = OFFLINE_REGION_PROTOCOL;
|
|
22
|
+
const SCHEME_PREFIX = `${OFFLINE_REGION_TILE_SCHEME}://`;
|
|
23
|
+
/** Build the tile-URL template a MapLibre source uses to address a region. */
|
|
24
|
+
export function buildOfflineRegionTileUrlTemplate(options = {}) {
|
|
25
|
+
const set = options.tileMatrixSetId ?? OFFLINE_REGION_DEFAULT_TILE_MATRIX_SET;
|
|
26
|
+
if (typeof set !== "string" || set.length === 0 || set.includes("/")) {
|
|
27
|
+
throw new HonuaOfflineRegionError("invalid-manifest", "tileMatrixSetId must be a non-empty path segment.", {
|
|
28
|
+
path: "tileMatrixSetId",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return `${SCHEME_PREFIX}${set}/{z}/{x}/{y}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parse an `offline-region://` tile URL.
|
|
35
|
+
*
|
|
36
|
+
* Parsing is a bounded segment split with no regular expression, and a URL
|
|
37
|
+
* carrying a query or fragment is refused outright: a tile address is an
|
|
38
|
+
* identity, and anything that could smuggle a token into one is not a tile
|
|
39
|
+
* address.
|
|
40
|
+
*/
|
|
41
|
+
export function parseOfflineRegionTileUrl(url) {
|
|
42
|
+
if (typeof url !== "string" || !url.startsWith(SCHEME_PREFIX)) {
|
|
43
|
+
throw invalid(`Tile URL must start with "${SCHEME_PREFIX}".`);
|
|
44
|
+
}
|
|
45
|
+
const rest = url.slice(SCHEME_PREFIX.length);
|
|
46
|
+
if (rest.includes("?") || rest.includes("#") || rest.includes("@")) {
|
|
47
|
+
throw invalid("Tile URL must not carry a query, fragment, or user information.");
|
|
48
|
+
}
|
|
49
|
+
const segments = rest.split("/");
|
|
50
|
+
if (segments.length !== 4)
|
|
51
|
+
throw invalid("Tile URL must be <tileMatrixSetId>/<z>/<x>/<y>.");
|
|
52
|
+
const [set, z, x, rawY] = segments;
|
|
53
|
+
if (set.length === 0)
|
|
54
|
+
throw invalid("Tile URL must name a tile matrix set.");
|
|
55
|
+
const dot = rawY.lastIndexOf(".");
|
|
56
|
+
const y = dot > 0 ? rawY.slice(0, dot) : rawY;
|
|
57
|
+
return { tileMatrixSetId: set, z: segment(z, "z"), x: segment(x, "x"), y: segment(y, "y") };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Create a MapLibre protocol handler that serves tiles from a persisted region.
|
|
61
|
+
*
|
|
62
|
+
* Register it with the same registrar the runtime uses for other custom schemes:
|
|
63
|
+
* `registrar.addProtocol(OFFLINE_REGION_TILE_SCHEME, handler)`.
|
|
64
|
+
*/
|
|
65
|
+
export function createOfflineRegionTileProtocol(options) {
|
|
66
|
+
if (!options?.manifest)
|
|
67
|
+
throw new TypeError("An offline region manifest is required.");
|
|
68
|
+
if (!options.store)
|
|
69
|
+
throw new TypeError("An offline region store is required.");
|
|
70
|
+
const { manifest, onTile, ...read } = options;
|
|
71
|
+
return async (request, abortController) => {
|
|
72
|
+
if (typeof request?.url !== "string")
|
|
73
|
+
throw new TypeError("request.url must be a string.");
|
|
74
|
+
const address = parseOfflineRegionTileUrl(request.url);
|
|
75
|
+
const tileRead = await readOfflineRegionTile(manifest, {
|
|
76
|
+
...read,
|
|
77
|
+
tile: { z: address.z, x: address.x, y: address.y },
|
|
78
|
+
tileMatrixSetId: address.tileMatrixSetId,
|
|
79
|
+
...(abortController ? { signal: abortController.signal } : {}),
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
onTile?.(tileRead);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Diagnostics are observational and never change what the map renders.
|
|
86
|
+
}
|
|
87
|
+
const bytes = tileRead.bytes;
|
|
88
|
+
const data = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
89
|
+
// The region is the cache. Declining a second layer of renderer caching keeps
|
|
90
|
+
// eviction, freshness, and provenance answerable from one place.
|
|
91
|
+
return { data, cacheControl: "no-store" };
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Register the offline tile protocol and return its disposer.
|
|
96
|
+
*
|
|
97
|
+
* The registrar is the application's — usually `maplibre-gl` itself — so the SDK
|
|
98
|
+
* neither imports the peer nor decides when a map should read from a region.
|
|
99
|
+
*/
|
|
100
|
+
export function registerOfflineRegionTileProtocol(registrar, options) {
|
|
101
|
+
if (typeof registrar?.addProtocol !== "function") {
|
|
102
|
+
throw new TypeError("A MapLibre-style protocol registrar with addProtocol is required.");
|
|
103
|
+
}
|
|
104
|
+
registrar.addProtocol(OFFLINE_REGION_TILE_SCHEME, createOfflineRegionTileProtocol(options));
|
|
105
|
+
return () => registrar.removeProtocol?.(OFFLINE_REGION_TILE_SCHEME);
|
|
106
|
+
}
|
|
107
|
+
function segment(value, path) {
|
|
108
|
+
if (value.length === 0 || value.length > 12)
|
|
109
|
+
throw invalid(`Tile URL ${path} is not an integer.`);
|
|
110
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
111
|
+
const code = value.charCodeAt(index);
|
|
112
|
+
if (code < 48 || code > 57)
|
|
113
|
+
throw invalid(`Tile URL ${path} is not an integer.`);
|
|
114
|
+
}
|
|
115
|
+
return Number(value);
|
|
116
|
+
}
|
|
117
|
+
function invalid(message) {
|
|
118
|
+
return new HonuaOfflineRegionError("invalid-manifest", message, { path: "url" });
|
|
119
|
+
}
|