@tileflow/react-native 0.1.0-alpha.7 → 0.1.0-alpha.9

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/dist/index.js CHANGED
@@ -15,16 +15,25 @@ import {
15
15
  Map as NativeMap
16
16
  } from "@maplibre/maplibre-react-native";
17
17
  import {
18
- Fragment,
18
+ Fragment as Fragment2,
19
19
  useImperativeHandle,
20
20
  useLayoutEffect as useLayoutEffect3,
21
21
  useMemo as useMemo3,
22
- useRef,
22
+ useRef as useRef2,
23
23
  useState as useState2,
24
24
  useSyncExternalStore
25
25
  } from "react";
26
26
  import { AppState, findNodeHandle, View as View2 } from "react-native";
27
27
 
28
+ // src/native-annotation-clusters.ts
29
+ import {
30
+ createTileflowAnnotationClusterIndex
31
+ } from "@tileflow/interactions";
32
+ import { withTileflowClusterLabelFont } from "@tileflow/interactions";
33
+ function createNativeAnnotationClusters(annotations, options) {
34
+ return createTileflowAnnotationClusterIndex(annotations, options);
35
+ }
36
+
28
37
  // src/native-interaction-input.ts
29
38
  import {
30
39
  tileflowInteractionJsonValueSchema,
@@ -36,6 +45,7 @@ var messages = {
36
45
  DUPLICATE_ANNOTATION_ID: "Annotation IDs must be unique.",
37
46
  INVALID_FIELD: "An interaction field is invalid.",
38
47
  INVALID_ANNOTATION: "An annotation is invalid.",
48
+ INVALID_DATA_LAYER: "A data source or overlay is invalid.",
39
49
  INVALID_DOCUMENT: "The interaction document is invalid.",
40
50
  LIMIT_EXCEEDED: "The portable interaction limit was exceeded.",
41
51
  MISSING_VIEW: "The interaction view is unavailable.",
@@ -828,8 +838,8 @@ function item(value, index) {
828
838
  return descriptor.value;
829
839
  }
830
840
  function array2(value, maximum2) {
831
- const count = length(value, maximum2);
832
- return Array.from({ length: count }, (_, index) => item(value, index));
841
+ const count2 = length(value, maximum2);
842
+ return Array.from({ length: count2 }, (_, index) => item(value, index));
833
843
  }
834
844
  function name2(value) {
835
845
  return typeof value === "string" && value.length > 0 && value.length <= 256;
@@ -914,8 +924,8 @@ function createNativeInteractionQuery(style, current, query) {
914
924
  cancel();
915
925
  return;
916
926
  }
917
- const count = length(hits);
918
- const read = Math.min(count, nativePoiQueryLimit + 1 - features.length);
927
+ const count2 = length(hits);
928
+ const read = Math.min(count2, nativePoiQueryLimit + 1 - features.length);
919
929
  for (let index = 0; index < read; index++) {
920
930
  const feature = item(hits, index);
921
931
  if (nativeInteractionField(feature, "type") !== "Feature") throw new Error();
@@ -1501,10 +1511,245 @@ function NativeBoundaryFeatureState({
1501
1511
  );
1502
1512
  }
1503
1513
 
1514
+ // src/native-data-layers.tsx
1515
+ import {
1516
+ GeoJSONSource,
1517
+ Layer,
1518
+ Marker
1519
+ } from "@maplibre/maplibre-react-native";
1520
+ import { useRef } from "react";
1521
+ import { Pressable, Text } from "react-native";
1522
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
1523
+ var bubble = Object.freeze({
1524
+ alignItems: "center",
1525
+ backgroundColor: "#2563eb",
1526
+ borderColor: "#ffffff",
1527
+ borderRadius: 22,
1528
+ borderWidth: 2,
1529
+ justifyContent: "center",
1530
+ minHeight: 44,
1531
+ minWidth: 44,
1532
+ paddingHorizontal: 6
1533
+ });
1534
+ var count = Object.freeze({ color: "#ffffff", fontSize: 14, fontWeight: "600" });
1535
+ function NativeDataLayers({
1536
+ camera,
1537
+ expand,
1538
+ onActivate,
1539
+ plan
1540
+ }) {
1541
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1542
+ plan.sources.map((source) => /* @__PURE__ */ jsx2(
1543
+ NativeDataSource,
1544
+ {
1545
+ camera,
1546
+ expand,
1547
+ onActivate,
1548
+ source
1549
+ },
1550
+ source.id
1551
+ )),
1552
+ plan.layers.map((layer) => /* @__PURE__ */ jsx2(Layer, { ...layer }, layer.id))
1553
+ ] });
1554
+ }
1555
+ function NativeDataSource({
1556
+ camera,
1557
+ expand,
1558
+ onActivate,
1559
+ source
1560
+ }) {
1561
+ const ref = useRef(null);
1562
+ const pressable = source.clusterLayers.length > 0 || source.pointLayers.length > 0;
1563
+ return /* @__PURE__ */ jsx2(
1564
+ GeoJSONSource,
1565
+ {
1566
+ ...source.definition,
1567
+ id: source.id,
1568
+ ref,
1569
+ data: source.data,
1570
+ onPress: pressable ? (event) => {
1571
+ const feature = event.nativeEvent.features[0];
1572
+ const coordinate2 = pointCoordinate(feature);
1573
+ if (!feature || !coordinate2) return;
1574
+ const properties2 = jsonProperties(feature.properties);
1575
+ const clusterId = properties2.cluster_id;
1576
+ if (typeof clusterId !== "number") {
1577
+ const layerId = source.pointLayers[0];
1578
+ if (!layerId) return;
1579
+ onActivate({
1580
+ coordinate: coordinate2,
1581
+ feature: { ...feature.id === void 0 ? {} : { id: feature.id }, properties: properties2 },
1582
+ kind: "style-feature",
1583
+ layerId
1584
+ });
1585
+ return;
1586
+ }
1587
+ onActivate({
1588
+ clusterId,
1589
+ coordinate: coordinate2,
1590
+ kind: "cluster",
1591
+ pointCount: typeof properties2.point_count === "number" ? properties2.point_count : 0,
1592
+ properties: properties2,
1593
+ source: source.id
1594
+ });
1595
+ if (!expand || !source.expandOnClick) return;
1596
+ ref.current?.getClusterExpansionZoom(clusterId).then(
1597
+ (zoom) => camera.current?.easeTo({
1598
+ center: [coordinate2[0], coordinate2[1]],
1599
+ duration: 400,
1600
+ zoom
1601
+ }),
1602
+ () => void 0
1603
+ );
1604
+ } : void 0,
1605
+ children: source.layers.map((layer) => /* @__PURE__ */ jsx2(Layer, { ...layer }, layer.id))
1606
+ }
1607
+ );
1608
+ }
1609
+ function NativeClusterMarker({
1610
+ cluster,
1611
+ label,
1612
+ onPress
1613
+ }) {
1614
+ const press = (event) => {
1615
+ event?.stopPropagation();
1616
+ onPress();
1617
+ };
1618
+ return /* @__PURE__ */ jsx2(
1619
+ Marker,
1620
+ {
1621
+ id: `tileflow-cluster-${cluster.clusterId}`,
1622
+ lngLat: [cluster.coordinate[0], cluster.coordinate[1]],
1623
+ accessible: true,
1624
+ accessibilityLabel: label,
1625
+ accessibilityRole: "button",
1626
+ onAccessibilityTap: onPress,
1627
+ onPress: press,
1628
+ children: /* @__PURE__ */ jsx2(Pressable, { accessible: false, onPress: press, style: bubble, children: /* @__PURE__ */ jsx2(Text, { style: count, children: cluster.label }) })
1629
+ }
1630
+ );
1631
+ }
1632
+ function pointCoordinate(feature) {
1633
+ const coordinates = feature?.geometry?.coordinates;
1634
+ return feature?.geometry?.type === "Point" && Array.isArray(coordinates) && typeof coordinates[0] === "number" && typeof coordinates[1] === "number" ? [coordinates[0], coordinates[1]] : void 0;
1635
+ }
1636
+ function jsonProperties(properties2) {
1637
+ return Object.fromEntries(
1638
+ Object.entries(properties2 ?? {}).filter(([, value]) => value !== void 0)
1639
+ );
1640
+ }
1641
+
1642
+ // src/native-data-layers-plan.ts
1643
+ var clusterMetadataKey = "tileflow:cluster";
1644
+ var placementMetadataKey = "tileflow:overlay-placement-manifest";
1645
+ function planNativeDataLayers(input) {
1646
+ const styleSources = input.style.sources ?? {};
1647
+ const styleLayers = input.style.layers ?? [];
1648
+ const styleLayerIds = new Set(styleLayers.map(({ id }) => String(id)));
1649
+ const anchors = input.style.metadata?.[placementMetadataKey]?.anchors;
1650
+ const diagnostics = [];
1651
+ const entries = /* @__PURE__ */ new Map();
1652
+ const freeLayers = [];
1653
+ const entry = (id) => entries.get(id);
1654
+ const addRoles = (sourceId, layer) => {
1655
+ const target = entry(sourceId);
1656
+ const cluster = layer.metadata?.[clusterMetadataKey];
1657
+ if (!target || !cluster) return;
1658
+ if (cluster.role === "cluster") target.clusterLayers.push(String(layer.id));
1659
+ if (cluster.role === "point") target.pointLayers.push(String(layer.id));
1660
+ if (cluster.expandOnClick === false) target.expandOnClick = false;
1661
+ };
1662
+ for (const [id, source] of Object.entries(input.dataSources ?? {}).sort(compare)) {
1663
+ const compiled2 = styleSources[id];
1664
+ if (source.kind !== "geojson" || compiled2 && compiled2.type !== "geojson") {
1665
+ diagnostics.push({ message: `Source "${id}" is not GeoJSON.`, path: `/dataSources/${id}` });
1666
+ continue;
1667
+ }
1668
+ const { data, kind: _kind, ...definition } = source;
1669
+ entries.set(id, {
1670
+ adopted: compiled2 !== void 0,
1671
+ clusterLayers: [],
1672
+ data,
1673
+ // A reused source keeps its compiled cluster options.
1674
+ definition: compiled2 ? {} : definition,
1675
+ expandOnClick: true,
1676
+ layers: [],
1677
+ pointLayers: []
1678
+ });
1679
+ }
1680
+ for (const layer of styleLayers) {
1681
+ const sourceId = typeof layer.source === "string" ? layer.source : void 0;
1682
+ const cluster = layer.metadata?.[clusterMetadataKey];
1683
+ if (!sourceId || !cluster || entries.has(sourceId)) continue;
1684
+ const compiled2 = styleSources[sourceId];
1685
+ if (compiled2?.type !== "geojson") continue;
1686
+ entries.set(sourceId, {
1687
+ adopted: true,
1688
+ clusterLayers: [],
1689
+ data: compiled2.data,
1690
+ definition: {},
1691
+ expandOnClick: true,
1692
+ layers: [],
1693
+ pointLayers: []
1694
+ });
1695
+ }
1696
+ for (const layer of styleLayers) {
1697
+ if (typeof layer.source === "string") addRoles(layer.source, layer);
1698
+ }
1699
+ for (const [id, overlay] of Object.entries(input.dataOverlays ?? {}).sort(compare)) {
1700
+ const path = `/dataOverlays/${id}`;
1701
+ if (overlay.kind !== "maplibre-overlay") {
1702
+ diagnostics.push({ message: `Overlay "${id}" is not a Tileflow overlay.`, path });
1703
+ continue;
1704
+ }
1705
+ const target = entry(overlay.source);
1706
+ if (!target && !styleSources[overlay.source]) {
1707
+ diagnostics.push({
1708
+ message: `Overlay "${id}" references unknown source "${overlay.source}".`,
1709
+ path: `${path}/source`
1710
+ });
1711
+ continue;
1712
+ }
1713
+ const anchor = anchors?.[overlay.placement];
1714
+ const beforeId = typeof anchor === "string" && styleLayerIds.has(anchor) ? anchor : void 0;
1715
+ for (const layer of overlay.layers) {
1716
+ if (styleLayerIds.has(layer.id)) continue;
1717
+ const prepared = withTileflowClusterLabelFont(layer, styleLayers);
1718
+ if (!prepared) {
1719
+ diagnostics.push({
1720
+ message: `Layer "${layer.id}" needs font: the style has no label font.`,
1721
+ path
1722
+ });
1723
+ continue;
1724
+ }
1725
+ const native = {
1726
+ ...prepared,
1727
+ ...beforeId ? { beforeId } : {},
1728
+ id: layer.id,
1729
+ source: overlay.source
1730
+ };
1731
+ if (target) {
1732
+ target.layers.push(native);
1733
+ addRoles(overlay.source, layer);
1734
+ } else {
1735
+ freeLayers.push(native);
1736
+ }
1737
+ }
1738
+ }
1739
+ return Object.freeze({
1740
+ diagnostics: Object.freeze(diagnostics),
1741
+ layers: Object.freeze(freeLayers),
1742
+ sources: Object.freeze([...entries].map(([id, value]) => Object.freeze({ id, ...value })))
1743
+ });
1744
+ }
1745
+ function compare([left], [right]) {
1746
+ return left < right ? -1 : left > right ? 1 : 0;
1747
+ }
1748
+
1504
1749
  // src/native-marker.tsx
1505
1750
  import { Marker as NativeMarker } from "@maplibre/maplibre-react-native";
1506
1751
  import { useLayoutEffect as useLayoutEffect2, useMemo as useMemo2 } from "react";
1507
- import { Pressable, processColor, View } from "react-native";
1752
+ import { Pressable as Pressable2, processColor, View } from "react-native";
1508
1753
 
1509
1754
  // src/native-marker-model.ts
1510
1755
  import { isValidElement } from "react";
@@ -1550,7 +1795,7 @@ function resolveNativeMarkerContent(context, render) {
1550
1795
  }
1551
1796
 
1552
1797
  // src/native-marker.tsx
1553
- import { jsx as jsx2 } from "react/jsx-runtime";
1798
+ import { jsx as jsx3 } from "react/jsx-runtime";
1554
1799
  var button = Object.freeze({
1555
1800
  minWidth: 44,
1556
1801
  minHeight: 44,
@@ -1593,7 +1838,7 @@ function NativeAnnotationMarker({
1593
1838
  event?.stopPropagation();
1594
1839
  lifecycle.markerPress(sceneKey, annotation);
1595
1840
  };
1596
- return /* @__PURE__ */ jsx2(
1841
+ return /* @__PURE__ */ jsx3(
1597
1842
  NativeMarker,
1598
1843
  {
1599
1844
  id: `tileflow-annotation-${model.key}`,
@@ -1604,8 +1849,8 @@ function NativeAnnotationMarker({
1604
1849
  lifecycle.beginTouch(sceneKey);
1605
1850
  press();
1606
1851
  },
1607
- children: /* @__PURE__ */ jsx2(
1608
- Pressable,
1852
+ children: /* @__PURE__ */ jsx3(
1853
+ Pressable2,
1609
1854
  {
1610
1855
  accessible: false,
1611
1856
  disabled: !enabled,
@@ -1615,14 +1860,14 @@ function NativeAnnotationMarker({
1615
1860
  event.stopPropagation();
1616
1861
  },
1617
1862
  onPress: press,
1618
- children: /* @__PURE__ */ jsx2(
1863
+ children: /* @__PURE__ */ jsx3(
1619
1864
  View,
1620
1865
  {
1621
1866
  pointerEvents: "none",
1622
1867
  accessible: false,
1623
1868
  accessibilityElementsHidden: true,
1624
1869
  importantForAccessibility: "no-hide-descendants",
1625
- children: rendered.content ?? /* @__PURE__ */ jsx2(View, { style: [dot, { backgroundColor: color }] })
1870
+ children: rendered.content ?? /* @__PURE__ */ jsx3(View, { style: [dot, { backgroundColor: color }] })
1626
1871
  }
1627
1872
  )
1628
1873
  }
@@ -1965,6 +2210,8 @@ var RETRY_LIMIT_MS = 6e4;
1965
2210
  var RETRY_AFTER_LIMIT_MS = 15 * 60 * 1e3;
1966
2211
  var RETRY_AFTER_SECONDS = /^\d{1,10}$/u;
1967
2212
  var QUOTA_CODES = /* @__PURE__ */ new Set(["COMMERCIAL_MAP_LIMIT_REACHED", "COMMERCIAL_SPENDING_LIMIT_REACHED"]);
2213
+ var SERVICE_CODE = /^[A-Z][A-Z0-9_]{0,63}$/u;
2214
+ var SERVICE_CODE_BODY_LIMIT = 4096;
1968
2215
  var MOBILE_CREDENTIAL = /^tf_public_[0-9a-f]{48}$/u;
1969
2216
  var MAP_ID = /^map_[A-Za-z0-9_-]{16}$/u;
1970
2217
  var CREDENTIAL_ID = /^key_[A-Za-z0-9_-]{8,80}$/u;
@@ -2008,7 +2255,8 @@ var TERMINAL_CODES = /* @__PURE__ */ new Set([
2008
2255
  "NATIVE_SESSION_CREDENTIAL_REJECTED",
2009
2256
  "NATIVE_SESSION_MAP_MISMATCH",
2010
2257
  "NATIVE_SESSION_MAP_NOT_FOUND",
2011
- "NATIVE_SESSION_QUOTA_EXCEEDED"
2258
+ "NATIVE_SESSION_QUOTA_EXCEEDED",
2259
+ "NATIVE_SESSION_REJECTED"
2012
2260
  ]);
2013
2261
  var ERROR_MESSAGES = {
2014
2262
  NATIVE_SESSION_INPUT_INVALID: "Native session configuration is invalid.",
@@ -2027,13 +2275,16 @@ var ERROR_MESSAGES = {
2027
2275
  NATIVE_SESSION_DISPOSED: "Native session controller is disposed."
2028
2276
  };
2029
2277
  var HostedNativeSessionError = class extends Error {
2030
- constructor(code) {
2278
+ constructor(code, reason) {
2031
2279
  super(ERROR_MESSAGES[code]);
2032
2280
  __publicField(this, "code");
2033
2281
  __publicField(this, "kind");
2034
2282
  this.name = "HostedNativeSessionError";
2035
2283
  this.code = code;
2036
2284
  this.kind = errorKind(code);
2285
+ if (code === "NATIVE_SESSION_REJECTED" && typeof reason === "string" && SERVICE_CODE.test(reason)) {
2286
+ this.reason = reason;
2287
+ }
2037
2288
  }
2038
2289
  };
2039
2290
  function createHostedNativeSessionController(input) {
@@ -2162,10 +2413,10 @@ function createHostedNativeSessionController(input) {
2162
2413
  if (retryAt === null || at >= retryAt) failures = Math.min(failures + 1, 16);
2163
2414
  const backoff = Math.min(RETRY_INITIAL_MS * 2 ** (failures - 1), RETRY_LIMIT_MS);
2164
2415
  retryAt = Math.max(retryAt ?? at, at + Math.max(backoff, retryAfterDelays.get(error) ?? 0));
2165
- retryError = Object.freeze({ code: error.code, kind: error.kind });
2416
+ retryError = safeError(error);
2166
2417
  }
2167
2418
  function rotateIfRequired(at) {
2168
- if (terminal) throw new HostedNativeSessionError(terminal.code);
2419
+ if (terminal) throw errorFrom(terminal);
2169
2420
  if (!activeSession || binding.kind !== "hosted") return;
2170
2421
  if (at - activeSession.startedAt < ROTATION_AGE_MS && activeSession.requestCount < ROTATION_REQUEST_LIMIT) {
2171
2422
  return;
@@ -2199,7 +2450,7 @@ function createHostedNativeSessionController(input) {
2199
2450
  return current;
2200
2451
  }
2201
2452
  async function ensureAuthority(session, at) {
2202
- if (terminal) throw new HostedNativeSessionError(terminal.code);
2453
+ if (terminal) throw errorFrom(terminal);
2203
2454
  if (session.authority && session.authority.clockEpoch === clock.epoch && session.authority.validUntil - at > REFRESH_MARGIN_MS) {
2204
2455
  return { authority: session.authority, session };
2205
2456
  }
@@ -2211,7 +2462,7 @@ function createHostedNativeSessionController(input) {
2211
2462
  if (session.authority && session.authority.clockEpoch === clock.epoch && session.authority.validUntil > at) {
2212
2463
  return { authority: session.authority, session };
2213
2464
  }
2214
- throw new HostedNativeSessionError(retryError.code);
2465
+ throw errorFrom(retryError);
2215
2466
  }
2216
2467
  const operationBinding = binding;
2217
2468
  const abort = new SessionAbortController();
@@ -2238,14 +2489,16 @@ function createHostedNativeSessionController(input) {
2238
2489
  const normalized = abort.signal.aborted ? abort.error() : normalizeError(error);
2239
2490
  const target = latestSession(session);
2240
2491
  if (normalized.kind === "terminal" && binding === operationBinding && !terminal) {
2241
- const failure3 = Object.freeze({ code: normalized.code, kind: normalized.kind });
2492
+ const failure3 = safeError(normalized);
2242
2493
  terminal = failure3;
2243
2494
  const rejectedSession = activeSession;
2244
2495
  if (rejectedSession) {
2245
2496
  rejectedSession.authority = null;
2246
2497
  rejectedSession.error = failure3;
2247
2498
  }
2248
- for (const pending of [...operations]) pending.abort(failure3.code);
2499
+ for (const pending of [...operations]) {
2500
+ pending.abort(failure3.code, failure3.reason);
2501
+ }
2249
2502
  if (binding === operationBinding && rejectedSession) {
2250
2503
  publishSession(rejectedSession, "error", failure3);
2251
2504
  }
@@ -2254,7 +2507,7 @@ function createHostedNativeSessionController(input) {
2254
2507
  delayRetry(normalized);
2255
2508
  }
2256
2509
  if (normalized.code !== "NATIVE_SESSION_REPLACED" && normalized.code !== "NATIVE_SESSION_DISPOSED") {
2257
- target.error = Object.freeze({ code: normalized.code, kind: normalized.kind });
2510
+ target.error = safeError(normalized);
2258
2511
  publishSession(target, "error", target.error);
2259
2512
  }
2260
2513
  throw normalized;
@@ -2280,7 +2533,7 @@ function createHostedNativeSessionController(input) {
2280
2533
  if (abort.signal.aborted) throw abort.error();
2281
2534
  const expectedSurfaceId = session.authority?.public.surfaceId ?? session.requestedSurfaceId;
2282
2535
  const response = await fetchResponse(hostedBinding, session, expectedSurfaceId, abort);
2283
- const source = await readBoundedResponse(response, abort);
2536
+ const { source, size } = await readBoundedResponse(response, abort);
2284
2537
  let body;
2285
2538
  try {
2286
2539
  body = source.length ? JSON.parse(source) : null;
@@ -2290,14 +2543,15 @@ function createHostedNativeSessionController(input) {
2290
2543
  }
2291
2544
  body = null;
2292
2545
  }
2293
- if (response.status === 409 && allowRestart && isExactRestart(body, session.sessionId)) {
2546
+ if (response.status === 409 && isExactRestart(body, session.sessionId)) {
2547
+ if (!allowRestart) throw new HostedNativeSessionError("NATIVE_SESSION_UNAVAILABLE");
2294
2548
  assertCurrentSession(session, hostedBinding);
2295
2549
  const replacement = redirectSession(session, readClock());
2296
2550
  if (abort.signal.aborted) throw abort.error();
2297
2551
  return bootstrap(replacement, hostedBinding, abort, false, false);
2298
2552
  }
2299
2553
  if (response.status !== 201) {
2300
- const failure3 = statusError(response.status, body);
2554
+ const failure3 = statusError(response.status, body, size);
2301
2555
  const delay = retryAfterMs(response.headers, now);
2302
2556
  if (delay !== null) retryAfterDelays.set(failure3, delay);
2303
2557
  throw failure3;
@@ -2435,7 +2689,7 @@ function createHostedNativeSessionController(input) {
2435
2689
  if (bindingGeneration !== expectedGeneration) {
2436
2690
  throw new HostedNativeSessionError("NATIVE_SESSION_REPLACED");
2437
2691
  }
2438
- if (terminal) throw new HostedNativeSessionError(terminal.code);
2692
+ if (terminal) throw errorFrom(terminal);
2439
2693
  checkedAt = readClock();
2440
2694
  if (resolved.authority.clockEpoch === clock.epoch && resolved.authority.validUntil > checkedAt) {
2441
2695
  admittedAuthorities.set(resolved.authority.public, resolved.authority);
@@ -2510,7 +2764,7 @@ function createHostedNativeSessionController(input) {
2510
2764
  if (disposed) throw new HostedNativeSessionError("NATIVE_SESSION_DISPOSED");
2511
2765
  lifecycle = "foreground";
2512
2766
  if (terminal) {
2513
- const error = new HostedNativeSessionError(terminal.code);
2767
+ const error = errorFrom(terminal);
2514
2768
  publish(currentSnapshot());
2515
2769
  throw error;
2516
2770
  }
@@ -2783,7 +3037,7 @@ async function readBoundedResponse(response, abort) {
2783
3037
  bytes.set(chunk, offset);
2784
3038
  offset += chunk.byteLength;
2785
3039
  }
2786
- return decodeUtf8(bytes);
3040
+ return { source: decodeUtf8(bytes), size };
2787
3041
  }
2788
3042
  function decodeUtf8(bytes) {
2789
3043
  let result = "";
@@ -2814,7 +3068,7 @@ function decodeUtf8(bytes) {
2814
3068
  if (index + remaining > bytes.length) {
2815
3069
  throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_UTF8_INVALID");
2816
3070
  }
2817
- for (let count = 0; count < remaining; count += 1) {
3071
+ for (let count2 = 0; count2 < remaining; count2 += 1) {
2818
3072
  const byte = bytes[index++];
2819
3073
  if ((byte & 192) !== 128) {
2820
3074
  throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_UTF8_INVALID");
@@ -2831,7 +3085,7 @@ function decodeUtf8(bytes) {
2831
3085
  function hasNoStore(value) {
2832
3086
  return typeof value === "string" && value.split(",").some((part) => part.trim().toLowerCase() === "no-store");
2833
3087
  }
2834
- function statusError(status, body) {
3088
+ function statusError(status, body, size) {
2835
3089
  if (status === 401) return new HostedNativeSessionError("NATIVE_SESSION_CREDENTIAL_REJECTED");
2836
3090
  if (status === 403) return new HostedNativeSessionError("NATIVE_SESSION_MAP_MISMATCH");
2837
3091
  if (status === 404) return new HostedNativeSessionError("NATIVE_SESSION_MAP_NOT_FOUND");
@@ -2843,7 +3097,21 @@ function statusError(status, body) {
2843
3097
  if (status === 408 || status >= 500 && status <= 599) {
2844
3098
  return new HostedNativeSessionError("NATIVE_SESSION_UNAVAILABLE");
2845
3099
  }
2846
- return new HostedNativeSessionError("NATIVE_SESSION_REJECTED");
3100
+ if (status >= 400 && status <= 499) {
3101
+ return new HostedNativeSessionError(
3102
+ "NATIVE_SESSION_REJECTED",
3103
+ size <= SERVICE_CODE_BODY_LIMIT && isRecord(body) && typeof body.code === "string" ? body.code : void 0
3104
+ );
3105
+ }
3106
+ return new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
3107
+ }
3108
+ function safeError(error) {
3109
+ return Object.freeze(
3110
+ error.reason === void 0 ? { code: error.code, kind: error.kind } : { code: error.code, kind: error.kind, reason: error.reason }
3111
+ );
3112
+ }
3113
+ function errorFrom(error) {
3114
+ return new HostedNativeSessionError(error.code, error.reason);
2847
3115
  }
2848
3116
  var retryAfterDelays = /* @__PURE__ */ new WeakMap();
2849
3117
  function retryAfterMs(headers, now) {
@@ -2893,12 +3161,13 @@ async function cancelResponse(response) {
2893
3161
  } catch {
2894
3162
  }
2895
3163
  }
2896
- var _aborted, _code, _listeners;
3164
+ var _aborted, _code, _reason, _listeners;
2897
3165
  var SessionAbortController = class {
2898
3166
  constructor() {
2899
3167
  __publicField(this, "signal");
2900
3168
  __privateAdd(this, _aborted, false);
2901
3169
  __privateAdd(this, _code, "NATIVE_SESSION_REPLACED");
3170
+ __privateAdd(this, _reason);
2902
3171
  __privateAdd(this, _listeners, /* @__PURE__ */ new Set());
2903
3172
  const isAborted = () => __privateGet(this, _aborted);
2904
3173
  this.signal = Object.freeze({
@@ -2913,9 +3182,10 @@ var SessionAbortController = class {
2913
3182
  }
2914
3183
  });
2915
3184
  }
2916
- abort(code) {
3185
+ abort(code, reason) {
2917
3186
  if (__privateGet(this, _aborted)) return;
2918
3187
  __privateSet(this, _code, code);
3188
+ __privateSet(this, _reason, reason);
2919
3189
  __privateSet(this, _aborted, true);
2920
3190
  for (const listener of [...__privateGet(this, _listeners)]) {
2921
3191
  try {
@@ -2926,11 +3196,12 @@ var SessionAbortController = class {
2926
3196
  __privateGet(this, _listeners).clear();
2927
3197
  }
2928
3198
  error() {
2929
- return new HostedNativeSessionError(__privateGet(this, _code));
3199
+ return new HostedNativeSessionError(__privateGet(this, _code), __privateGet(this, _reason));
2930
3200
  }
2931
3201
  };
2932
3202
  _aborted = new WeakMap();
2933
3203
  _code = new WeakMap();
3204
+ _reason = new WeakMap();
2934
3205
  _listeners = new WeakMap();
2935
3206
  function raceWithAbort(promise, controller) {
2936
3207
  if (controller.signal.aborted) return Promise.reject(controller.error());
@@ -3238,7 +3509,7 @@ function createNativeAdmissionOwner(options) {
3238
3509
  reported = code;
3239
3510
  if (state.status !== "error") return;
3240
3511
  try {
3241
- input.onSessionError?.(Object.freeze({ code: state.error.code, kind: state.error.kind }));
3512
+ input.onSessionError?.(Object.freeze({ ...state.error }));
3242
3513
  } catch {
3243
3514
  }
3244
3515
  });
@@ -3864,27 +4135,27 @@ function decode(bytes) {
3864
4135
  for (let index = 0; index < bytes.length; ) {
3865
4136
  const first = bytes[index++];
3866
4137
  let code;
3867
- let count;
4138
+ let count2;
3868
4139
  let minimum;
3869
4140
  if (first <= 127) {
3870
4141
  code = first;
3871
- count = 0;
4142
+ count2 = 0;
3872
4143
  minimum = 0;
3873
4144
  } else if (first >= 194 && first <= 223) {
3874
4145
  code = first & 31;
3875
- count = 1;
4146
+ count2 = 1;
3876
4147
  minimum = 128;
3877
4148
  } else if (first >= 224 && first <= 239) {
3878
4149
  code = first & 15;
3879
- count = 2;
4150
+ count2 = 2;
3880
4151
  minimum = 2048;
3881
4152
  } else if (first >= 240 && first <= 244) {
3882
4153
  code = first & 7;
3883
- count = 3;
4154
+ count2 = 3;
3884
4155
  minimum = 65536;
3885
4156
  } else throw new NativePreparationError();
3886
- if (index + count > bytes.length) throw new NativePreparationError();
3887
- for (let part = 0; part < count; part++) {
4157
+ if (index + count2 > bytes.length) throw new NativePreparationError();
4158
+ for (let part = 0; part < count2; part++) {
3888
4159
  const next = bytes[index++];
3889
4160
  if ((next & 192) !== 128) throw new NativePreparationError();
3890
4161
  code = code * 64 + (next & 63);
@@ -5568,7 +5839,7 @@ function fontMetadata(input, developmentOrigin) {
5568
5839
  }
5569
5840
  function validateFontFaces(faces, documentUrl, metadata, add, rewrites, policy, discriminate, developmentOrigin) {
5570
5841
  if (Object.keys(faces).length > nativePreparationLimits.fontFaces) throw invalid3();
5571
- let count = 0;
5842
+ let count2 = 0;
5572
5843
  const matched = /* @__PURE__ */ new Set();
5573
5844
  for (const name3 of Object.keys(faces).sort()) {
5574
5845
  if (name3.length === 0 || name3.length > 256 || name3 !== name3.trim()) throw invalid3();
@@ -5576,7 +5847,7 @@ function validateFontFaces(faces, documentUrl, metadata, add, rewrites, policy,
5576
5847
  if (!Array.isArray(definitions) || definitions.length === 0) throw invalid3();
5577
5848
  const rewritten = [];
5578
5849
  for (const definition of definitions) {
5579
- if (++count > nativePreparationLimits.fontFaces || !record2(definition)) throw invalid3();
5850
+ if (++count2 > nativePreparationLimits.fontFaces || !record2(definition)) throw invalid3();
5580
5851
  const keys = Object.keys(definition).sort();
5581
5852
  if (keys.length !== 4 || !["font-family", "font-style", "font-weight", "url"].every((key2) => keys.includes(key2)))
5582
5853
  throw invalid3();
@@ -5824,8 +6095,10 @@ var sessionErrorCodes = /* @__PURE__ */ new Set([
5824
6095
  "NATIVE_SESSION_MAP_MISMATCH",
5825
6096
  "NATIVE_SESSION_MAP_NOT_FOUND",
5826
6097
  "NATIVE_SESSION_QUOTA_EXCEEDED",
6098
+ "NATIVE_SESSION_REJECTED",
5827
6099
  "NATIVE_SESSION_INPUT_INVALID"
5828
6100
  ]);
6101
+ var SERVICE_CODE2 = /^[A-Z][A-Z0-9_]{0,63}$/u;
5829
6102
  function themeSource(source, name3) {
5830
6103
  const theme = Object.hasOwn(source.map.themes, name3) ? source.map.themes[name3] : void 0;
5831
6104
  if (!theme) throw new NativePreparationError();
@@ -6085,9 +6358,14 @@ function createMountedMapOwner(ports2) {
6085
6358
  reopenings += 1;
6086
6359
  restart(epoch);
6087
6360
  }
6088
- function sessionError(epoch, code) {
6361
+ function sessionError(epoch, code, reason) {
6089
6362
  if (disposed || current !== epoch || !epoch.live || !sessionErrorCodes.has(code)) return;
6090
- const error = Object.freeze({ code, kind: "terminal" });
6363
+ const detail = code === "NATIVE_SESSION_REJECTED" && typeof reason === "string" && SERVICE_CODE2.test(reason) ? { reason } : {};
6364
+ const error = Object.freeze({
6365
+ code,
6366
+ kind: "terminal",
6367
+ ...detail
6368
+ });
6091
6369
  emit({ type: "session-error", generation: generation(), error });
6092
6370
  }
6093
6371
  function createEpoch(developmentOrigin) {
@@ -6125,7 +6403,7 @@ function createMountedMapOwner(ports2) {
6125
6403
  onRetired: () => void Promise.resolve().then(() => reopen(epoch)),
6126
6404
  onSessionError: (error) => {
6127
6405
  if (error.kind !== "terminal") return;
6128
- void Promise.resolve().then(() => sessionError(epoch, error.code));
6406
+ void Promise.resolve().then(() => sessionError(epoch, error.code, error.reason));
6129
6407
  }
6130
6408
  });
6131
6409
  const map = await epoch.lease.ready;
@@ -7670,7 +7948,7 @@ function createNativeMapOwner() {
7670
7948
  }
7671
7949
 
7672
7950
  // src/map.tsx
7673
- import { jsx as jsx3, jsxs } from "react/jsx-runtime";
7951
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
7674
7952
  var fill = Object.freeze({ flex: 1 });
7675
7953
  function NativeScene({
7676
7954
  scene,
@@ -7680,17 +7958,18 @@ function NativeScene({
7680
7958
  interactions,
7681
7959
  interactionEnabled,
7682
7960
  renderMarker,
7683
- boundaryFeatureState
7961
+ boundaryFeatureState,
7962
+ data
7684
7963
  }) {
7685
- const view = useRef(null);
7686
- const camera = useRef(null);
7687
- const nativeMap = useRef(null);
7688
- const committedHost = useRef(void 0);
7689
- const tag = useRef(null);
7690
- const laidOut = useRef(false);
7691
- const styleLoaded = useRef(false);
7692
- const attachRequested = useRef(false);
7693
- const latestStyle = useRef(scene.style);
7964
+ const view = useRef2(null);
7965
+ const camera = useRef2(null);
7966
+ const nativeMap = useRef2(null);
7967
+ const committedHost = useRef2(void 0);
7968
+ const tag = useRef2(null);
7969
+ const laidOut = useRef2(false);
7970
+ const styleLoaded = useRef2(false);
7971
+ const attachRequested = useRef2(false);
7972
+ const latestStyle = useRef2(scene.style);
7694
7973
  latestStyle.current = scene.style;
7695
7974
  const [loadedStyle, setLoadedStyle] = useState2(void 0);
7696
7975
  const host = useMemo3(() => {
@@ -7745,7 +8024,42 @@ function NativeScene({
7745
8024
  };
7746
8025
  const annotations = interactions?.annotations;
7747
8026
  const byId = new globalThis.Map(annotations?.map((annotation) => [annotation.id, annotation]));
7748
- return /* @__PURE__ */ jsx3(
8027
+ const [zoomLevel, setZoomLevel] = useState2(() => Math.floor(scene.initialView.zoom));
8028
+ const followZoom = (event) => {
8029
+ if (Number.isFinite(event.nativeEvent.zoom)) setZoomLevel(Math.floor(event.nativeEvent.zoom));
8030
+ };
8031
+ const clusterSettings = data.annotationClusters;
8032
+ const clusterKey = JSON.stringify(clusterSettings ?? null);
8033
+ const clusterIndex = useMemo3(
8034
+ () => clusterSettings && annotations ? createNativeAnnotationClusters(annotations, clusterSettings) : void 0,
8035
+ // Options written inline are new objects each render; only a real change rebuilds groups.
8036
+ // eslint-disable-next-line react-hooks/exhaustive-deps
8037
+ [annotations, clusterKey]
8038
+ );
8039
+ const clusterView = clusterIndex?.at(zoomLevel);
8040
+ const styleReady = loadedStyle === scene.style;
8041
+ const dataPlan = useMemo3(
8042
+ () => planNativeDataLayers({
8043
+ dataOverlays: data.dataOverlays,
8044
+ dataSources: data.dataSources,
8045
+ style: scene.style
8046
+ }),
8047
+ [data.dataOverlays, data.dataSources, scene.style]
8048
+ );
8049
+ const reportedPlan = useRef2(void 0);
8050
+ useLayoutEffect3(() => {
8051
+ if (reportedPlan.current === dataPlan) return;
8052
+ reportedPlan.current = dataPlan;
8053
+ for (const { message, path } of dataPlan.diagnostics)
8054
+ data.onDiagnostic({ code: "INVALID_DATA_LAYER", level: "error", message, path });
8055
+ }, [data, dataPlan]);
8056
+ const activate = (target) => data.onActivate({
8057
+ coordinate: target.coordinate,
8058
+ inputModality: "touch",
8059
+ target,
8060
+ type: "target:activate"
8061
+ });
8062
+ return /* @__PURE__ */ jsx4(
7749
8063
  View2,
7750
8064
  {
7751
8065
  ref: bind2,
@@ -7762,13 +8076,15 @@ function NativeScene({
7762
8076
  lifecycle.layoutChanged(scene.key);
7763
8077
  attach();
7764
8078
  },
7765
- children: /* @__PURE__ */ jsxs(
8079
+ children: /* @__PURE__ */ jsxs2(
7766
8080
  NativeMap,
7767
8081
  {
7768
8082
  ...mapOptions,
7769
8083
  ref: nativeMap,
7770
8084
  style: fill,
7771
8085
  mapStyle,
8086
+ onRegionIsChanging: followZoom,
8087
+ onRegionDidChange: followZoom,
7772
8088
  onPress: (event) => {
7773
8089
  if (!host.current()) return;
7774
8090
  try {
@@ -7787,7 +8103,7 @@ function NativeScene({
7787
8103
  attach();
7788
8104
  },
7789
8105
  children: [
7790
- /* @__PURE__ */ jsx3(
8106
+ /* @__PURE__ */ jsx4(
7791
8107
  NativeCamera,
7792
8108
  {
7793
8109
  ref: camera,
@@ -7797,21 +8113,57 @@ function NativeScene({
7797
8113
  },
7798
8114
  "tileflow-camera"
7799
8115
  ),
7800
- /* @__PURE__ */ jsx3(Fragment, { children: interactions?.plan.order.map((id) => {
7801
- const annotation = byId.get(id);
7802
- return annotation ? /* @__PURE__ */ jsx3(
7803
- NativeAnnotationMarker,
8116
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
8117
+ interactions?.plan.order.map((id) => {
8118
+ const annotation = byId.get(id);
8119
+ return annotation && !clusterView?.hiddenAnnotationIds.has(id) ? /* @__PURE__ */ jsx4(
8120
+ NativeAnnotationMarker,
8121
+ {
8122
+ annotation,
8123
+ enabled: interactionEnabled,
8124
+ sceneKey: scene.key,
8125
+ lifecycle,
8126
+ renderMarker
8127
+ },
8128
+ annotation.id
8129
+ ) : null;
8130
+ }),
8131
+ clusterView?.clusters.map((cluster) => /* @__PURE__ */ jsx4(
8132
+ NativeClusterMarker,
7804
8133
  {
7805
- annotation,
7806
- enabled: interactionEnabled,
7807
- sceneKey: scene.key,
7808
- lifecycle,
7809
- renderMarker
8134
+ cluster,
8135
+ label: clusterSettings?.formatAriaLabel?.(cluster.pointCount) ?? `${cluster.pointCount} places`,
8136
+ onPress: () => {
8137
+ activate({
8138
+ annotationIds: cluster.annotationIds,
8139
+ clusterId: cluster.clusterId,
8140
+ coordinate: cluster.coordinate,
8141
+ kind: "cluster",
8142
+ pointCount: cluster.pointCount,
8143
+ properties: { point_count: cluster.pointCount }
8144
+ });
8145
+ if (data.expand && clusterSettings?.expandOnClick !== false)
8146
+ camera.current?.easeTo({
8147
+ center: [cluster.coordinate[0], cluster.coordinate[1]],
8148
+ duration: 400,
8149
+ zoom: cluster.expansionZoom
8150
+ });
8151
+ }
7810
8152
  },
7811
- annotation.id
7812
- ) : null;
7813
- }) }, "tileflow-annotations"),
7814
- boundaryFeatureState ? /* @__PURE__ */ jsx3(
8153
+ `cluster-${cluster.clusterId}`
8154
+ ))
8155
+ ] }, "tileflow-annotations"),
8156
+ styleReady ? /* @__PURE__ */ jsx4(
8157
+ NativeDataLayers,
8158
+ {
8159
+ camera,
8160
+ expand: data.expand,
8161
+ onActivate: activate,
8162
+ plan: dataPlan
8163
+ },
8164
+ "tileflow-data-layers"
8165
+ ) : null,
8166
+ boundaryFeatureState ? /* @__PURE__ */ jsx4(
7815
8167
  NativeBoundaryFeatureState,
7816
8168
  {
7817
8169
  input: boundaryFeatureState,
@@ -7836,6 +8188,22 @@ function Map2(props) {
7836
8188
  lifecycle.getSnapshot,
7837
8189
  lifecycle.getSnapshot
7838
8190
  );
8191
+ const latestProps = useRef2(props);
8192
+ latestProps.current = props;
8193
+ const [callbacks] = useState2(() => ({
8194
+ onActivate: (event) => latestProps.current.onInteractionEvent?.({ ...event, type: "target:activate" }),
8195
+ onDiagnostic: (diagnostic) => latestProps.current.onInteractionDiagnostic?.(diagnostic)
8196
+ }));
8197
+ const data = useMemo3(
8198
+ () => ({
8199
+ annotationClusters: props.annotationClusters,
8200
+ dataOverlays: props.dataOverlays,
8201
+ dataSources: props.dataSources,
8202
+ expand: props.view === void 0,
8203
+ ...callbacks
8204
+ }),
8205
+ [callbacks, props.annotationClusters, props.dataOverlays, props.dataSources, props.view]
8206
+ );
7839
8207
  useImperativeHandle(
7840
8208
  props.ref,
7841
8209
  () => Object.freeze({ getSourceState: lifecycle.getSourceState, retry: lifecycle.retry }),
@@ -7856,7 +8224,7 @@ function Map2(props) {
7856
8224
  useLayoutEffect3(() => {
7857
8225
  lifecycle.update(props);
7858
8226
  });
7859
- return /* @__PURE__ */ jsx3(View2, { style: props.style, testID: props.testID, children: snapshot.renderer ? /* @__PURE__ */ jsx3(
8227
+ return /* @__PURE__ */ jsx4(View2, { style: props.style, testID: props.testID, children: snapshot.renderer ? /* @__PURE__ */ jsx4(
7860
8228
  NativeScene,
7861
8229
  {
7862
8230
  scene: snapshot.renderer,
@@ -7866,6 +8234,7 @@ function Map2(props) {
7866
8234
  interactionEnabled: snapshot.interactionEnabled === true,
7867
8235
  renderMarker: props.renderMarker,
7868
8236
  boundaryFeatureState: props.boundaryFeatureState,
8237
+ data,
7869
8238
  children: props.children
7870
8239
  },
7871
8240
  snapshot.renderer.key