@honua/app-platform 0.1.11-beta.0 → 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.
Files changed (236) hide show
  1. package/columnar/apache-arrow.d.ts +17 -0
  2. package/columnar/apache-arrow.d.ts.map +1 -0
  3. package/columnar/apache-arrow.js +1036 -0
  4. package/columnar/batch-cache-conformance.d.ts +66 -0
  5. package/columnar/batch-cache-conformance.d.ts.map +1 -0
  6. package/columnar/batch-cache-conformance.js +317 -0
  7. package/columnar/batch-cache-indexeddb.d.ts +31 -0
  8. package/columnar/batch-cache-indexeddb.d.ts.map +1 -0
  9. package/columnar/batch-cache-indexeddb.js +148 -0
  10. package/columnar/batch-cache.d.ts +298 -0
  11. package/columnar/batch-cache.d.ts.map +1 -0
  12. package/columnar/batch-cache.js +757 -0
  13. package/columnar/geoarrow-aggregate.d.ts +178 -0
  14. package/columnar/geoarrow-aggregate.d.ts.map +1 -0
  15. package/columnar/geoarrow-aggregate.js +976 -0
  16. package/columnar/geoarrow-patch.d.ts +382 -0
  17. package/columnar/geoarrow-patch.d.ts.map +1 -0
  18. package/columnar/geoarrow-patch.js +1656 -0
  19. package/columnar/geoarrow-reproject.d.ts +22 -0
  20. package/columnar/geoarrow-reproject.d.ts.map +1 -0
  21. package/columnar/geoarrow-reproject.js +119 -0
  22. package/columnar/geoarrow-serialization.d.ts +68 -0
  23. package/columnar/geoarrow-serialization.d.ts.map +1 -0
  24. package/columnar/geoarrow-serialization.js +331 -0
  25. package/columnar/geoarrow-types.d.ts +271 -0
  26. package/columnar/geoarrow-types.d.ts.map +1 -0
  27. package/columnar/geoarrow-types.js +15 -0
  28. package/columnar/geoarrow.d.ts +52 -0
  29. package/columnar/geoarrow.d.ts.map +1 -0
  30. package/columnar/geoarrow.js +1469 -0
  31. package/columnar/index.d.ts +43 -0
  32. package/columnar/index.d.ts.map +1 -0
  33. package/columnar/index.js +29 -0
  34. package/columnar/operators.d.ts +70 -0
  35. package/columnar/operators.d.ts.map +1 -0
  36. package/columnar/operators.js +307 -0
  37. package/columnar/result.d.ts +369 -0
  38. package/columnar/result.d.ts.map +1 -0
  39. package/columnar/result.js +815 -0
  40. package/columnar/telemetry.d.ts +152 -0
  41. package/columnar/telemetry.d.ts.map +1 -0
  42. package/columnar/telemetry.js +265 -0
  43. package/columnar/transfer.d.ts +28 -0
  44. package/columnar/transfer.d.ts.map +1 -0
  45. package/columnar/transfer.js +669 -0
  46. package/columnar/types.d.ts +175 -0
  47. package/columnar/types.d.ts.map +1 -0
  48. package/columnar/types.js +22 -0
  49. package/columnar/worker.d.ts +157 -0
  50. package/columnar/worker.d.ts.map +1 -0
  51. package/columnar/worker.js +1059 -0
  52. package/contract/pmtiles.d.ts +6 -5
  53. package/contract/pmtiles.d.ts.map +1 -1
  54. package/contract/pmtiles.js +30 -5
  55. package/control-plane/generated/admin-operations.d.ts +3 -3
  56. package/control-plane/generated/admin-operations.d.ts.map +1 -1
  57. package/control-plane/generated/admin-operations.js +3 -3
  58. package/controls/catalog.d.ts.map +1 -1
  59. package/controls/catalog.js +110 -0
  60. package/controls/index.d.ts +1 -1
  61. package/controls/index.js +1 -1
  62. package/controls/types.d.ts +1 -1
  63. package/controls/types.js +1 -1
  64. package/core/client.d.ts.map +1 -1
  65. package/core/client.js +87 -19
  66. package/core/deprecation-notice.d.ts +28 -0
  67. package/core/deprecation-notice.d.ts.map +1 -0
  68. package/core/deprecation-notice.js +22 -0
  69. package/core/esri-geojson.d.ts.map +1 -1
  70. package/core/esri-geojson.js +10 -6
  71. package/core/optional-grpc-peer.d.ts +15 -0
  72. package/core/optional-grpc-peer.d.ts.map +1 -0
  73. package/core/optional-grpc-peer.js +60 -0
  74. package/esri-compat/basemap-gallery.d.ts +2 -0
  75. package/esri-compat/basemap-gallery.d.ts.map +1 -1
  76. package/esri-compat/basemap-gallery.js +12 -0
  77. package/esri-compat/basemap.d.ts +4 -0
  78. package/esri-compat/basemap.d.ts.map +1 -1
  79. package/esri-compat/basemap.js +70 -0
  80. package/esri-compat/bookmarks.d.ts +2 -0
  81. package/esri-compat/bookmarks.d.ts.map +1 -1
  82. package/esri-compat/bookmarks.js +12 -0
  83. package/esri-compat/controls.d.ts +18 -0
  84. package/esri-compat/controls.d.ts.map +1 -1
  85. package/esri-compat/controls.js +120 -57
  86. package/esri-compat/directions.d.ts +4 -0
  87. package/esri-compat/directions.d.ts.map +1 -1
  88. package/esri-compat/directions.js +30 -20
  89. package/esri-compat/editor.d.ts +4 -0
  90. package/esri-compat/editor.d.ts.map +1 -1
  91. package/esri-compat/editor.js +16 -0
  92. package/esri-compat/esri-config.d.ts +2 -1
  93. package/esri-compat/esri-config.d.ts.map +1 -1
  94. package/esri-compat/feature-effect.d.ts +23 -0
  95. package/esri-compat/feature-effect.d.ts.map +1 -0
  96. package/esri-compat/feature-effect.js +12 -0
  97. package/esri-compat/feature-form.d.ts +6 -0
  98. package/esri-compat/feature-form.d.ts.map +1 -1
  99. package/esri-compat/feature-form.js +63 -0
  100. package/esri-compat/feature-layer.d.ts +21 -2
  101. package/esri-compat/feature-layer.d.ts.map +1 -1
  102. package/esri-compat/feature-layer.js +172 -1
  103. package/esri-compat/feature-table.d.ts +5 -0
  104. package/esri-compat/feature-table.d.ts.map +1 -1
  105. package/esri-compat/feature-table.js +23 -0
  106. package/esri-compat/identity-manager.d.ts +1 -1
  107. package/esri-compat/identity-manager.js +1 -1
  108. package/esri-compat/legend.d.ts +2 -0
  109. package/esri-compat/legend.d.ts.map +1 -1
  110. package/esri-compat/legend.js +98 -0
  111. package/esri-compat/locator.d.ts +1 -1
  112. package/esri-compat/locator.js +1 -1
  113. package/esri-compat/map-view-mount.d.ts +1 -0
  114. package/esri-compat/map-view-mount.d.ts.map +1 -1
  115. package/esri-compat/map-view-mount.js +46 -25
  116. package/esri-compat/map-view.d.ts +13 -0
  117. package/esri-compat/map-view.d.ts.map +1 -1
  118. package/esri-compat/map-view.js +94 -8
  119. package/esri-compat/map.d.ts +5 -1
  120. package/esri-compat/map.d.ts.map +1 -1
  121. package/esri-compat/map.js +10 -0
  122. package/esri-compat/measurement.d.ts +2 -0
  123. package/esri-compat/measurement.d.ts.map +1 -1
  124. package/esri-compat/measurement.js +14 -0
  125. package/esri-compat/point.d.ts +7 -0
  126. package/esri-compat/point.d.ts.map +1 -1
  127. package/esri-compat/point.js +33 -3
  128. package/esri-compat/popup.d.ts +2 -0
  129. package/esri-compat/popup.d.ts.map +1 -1
  130. package/esri-compat/popup.js +12 -0
  131. package/esri-compat/portal.d.ts +1 -1
  132. package/esri-compat/portal.js +1 -1
  133. package/esri-compat/print.d.ts +3 -0
  134. package/esri-compat/print.d.ts.map +1 -1
  135. package/esri-compat/print.js +15 -0
  136. package/esri-compat/search.d.ts +4 -0
  137. package/esri-compat/search.d.ts.map +1 -1
  138. package/esri-compat/search.js +25 -28
  139. package/esri-compat/sketch.d.ts +2 -0
  140. package/esri-compat/sketch.d.ts.map +1 -1
  141. package/esri-compat/sketch.js +12 -0
  142. package/esri-compat/widget-host.d.ts +46 -17
  143. package/esri-compat/widget-host.d.ts.map +1 -1
  144. package/esri-compat/widget-host.js +84 -20
  145. package/esri-compat/widget-shell-hosts.d.ts +61 -0
  146. package/esri-compat/widget-shell-hosts.d.ts.map +1 -0
  147. package/esri-compat/widget-shell-hosts.js +76 -0
  148. package/kernel/renderer.d.ts +82 -0
  149. package/kernel/renderer.js +1 -0
  150. package/offline/conflict-resolution.d.ts +183 -0
  151. package/offline/conflict-resolution.d.ts.map +1 -0
  152. package/offline/conflict-resolution.js +286 -0
  153. package/offline/credential-screen.d.ts +34 -0
  154. package/offline/credential-screen.d.ts.map +1 -0
  155. package/offline/credential-screen.js +78 -0
  156. package/offline/digest.d.ts +15 -0
  157. package/offline/digest.d.ts.map +1 -0
  158. package/offline/digest.js +58 -0
  159. package/offline/edit-queue.d.ts +403 -0
  160. package/offline/edit-queue.d.ts.map +1 -0
  161. package/offline/edit-queue.js +1807 -0
  162. package/offline/edit-replay.d.ts +116 -0
  163. package/offline/edit-replay.d.ts.map +1 -0
  164. package/offline/edit-replay.js +392 -0
  165. package/offline/fetch-handler.d.ts +12 -0
  166. package/offline/fetch-handler.d.ts.map +1 -0
  167. package/offline/fetch-handler.js +65 -0
  168. package/offline/index.d.ts +52 -0
  169. package/offline/index.d.ts.map +1 -0
  170. package/offline/index.js +34 -0
  171. package/offline/indexeddb.d.ts +135 -0
  172. package/offline/indexeddb.d.ts.map +1 -0
  173. package/offline/indexeddb.js +888 -0
  174. package/offline/memory-store.d.ts +17 -0
  175. package/offline/memory-store.d.ts.map +1 -0
  176. package/offline/memory-store.js +183 -0
  177. package/offline/quota.d.ts +161 -0
  178. package/offline/quota.d.ts.map +1 -0
  179. package/offline/quota.js +370 -0
  180. package/offline/read-gate.d.ts +175 -0
  181. package/offline/read-gate.d.ts.map +1 -0
  182. package/offline/read-gate.js +252 -0
  183. package/offline/read-resource.d.ts +99 -0
  184. package/offline/read-resource.d.ts.map +1 -0
  185. package/offline/read-resource.js +156 -0
  186. package/offline/read.d.ts +50 -0
  187. package/offline/read.d.ts.map +1 -0
  188. package/offline/read.js +65 -0
  189. package/offline/region.d.ts +29 -0
  190. package/offline/region.d.ts.map +1 -0
  191. package/offline/region.js +1007 -0
  192. package/offline/replay-conflict.d.ts +183 -0
  193. package/offline/replay-conflict.d.ts.map +1 -0
  194. package/offline/replay-conflict.js +482 -0
  195. package/offline/selection.d.ts +140 -0
  196. package/offline/selection.d.ts.map +1 -0
  197. package/offline/selection.js +391 -0
  198. package/offline/snapshot.d.ts +132 -0
  199. package/offline/snapshot.d.ts.map +1 -0
  200. package/offline/snapshot.js +312 -0
  201. package/offline/status.d.ts +136 -0
  202. package/offline/status.d.ts.map +1 -0
  203. package/offline/status.js +479 -0
  204. package/offline/store-conformance.d.ts +48 -0
  205. package/offline/store-conformance.d.ts.map +1 -0
  206. package/offline/store-conformance.js +233 -0
  207. package/offline/tile-protocol.d.ts +81 -0
  208. package/offline/tile-protocol.d.ts.map +1 -0
  209. package/offline/tile-protocol.js +119 -0
  210. package/offline/types.d.ts +349 -0
  211. package/offline/types.d.ts.map +1 -0
  212. package/offline/types.js +75 -0
  213. package/package.json +14 -2
  214. package/scene-workspace/index.d.ts +2 -2
  215. package/scene-workspace/index.d.ts.map +1 -1
  216. package/scene-workspace/index.js +1 -1
  217. package/scene-workspace/scene-discovery.d.ts +111 -91
  218. package/scene-workspace/scene-discovery.d.ts.map +1 -1
  219. package/scene-workspace/scene-discovery.js +340 -92
  220. package/scene-workspace/scene-view.d.ts +6 -6
  221. package/scene-workspace/scene-view.d.ts.map +1 -1
  222. package/scene-workspace/scene-view.js +32 -6
  223. package/web-components/elements.d.ts +34 -0
  224. package/web-components/elements.d.ts.map +1 -1
  225. package/web-components/elements.js +82 -8
  226. package/web-components/index.d.ts +2 -0
  227. package/web-components/index.d.ts.map +1 -1
  228. package/web-components/index.js +3 -1
  229. package/web-components/map-chrome.d.ts +103 -0
  230. package/web-components/map-chrome.d.ts.map +1 -0
  231. package/web-components/map-chrome.js +325 -0
  232. package/web-components/widget-shell.d.ts +68 -0
  233. package/web-components/widget-shell.d.ts.map +1 -0
  234. package/web-components/widget-shell.js +224 -0
  235. package/widget-capabilities.d.ts +67 -0
  236. 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
+ }