@tileflow/react-native 0.1.0-alpha.6 → 0.1.0-alpha.8

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
@@ -17,10 +17,10 @@ import {
17
17
  import {
18
18
  Fragment,
19
19
  useImperativeHandle,
20
- useLayoutEffect as useLayoutEffect2,
21
- useMemo as useMemo2,
20
+ useLayoutEffect as useLayoutEffect3,
21
+ useMemo as useMemo3,
22
22
  useRef,
23
- useState,
23
+ useState as useState2,
24
24
  useSyncExternalStore
25
25
  } from "react";
26
26
  import { AppState, findNodeHandle, View as View2 } from "react-native";
@@ -1268,6 +1268,9 @@ function createMapLifecycle(create, retire) {
1268
1268
  getSourceState() {
1269
1269
  return active?.owner.getSourceState();
1270
1270
  },
1271
+ retry() {
1272
+ active?.owner.retry();
1273
+ },
1271
1274
  subscribe(listener) {
1272
1275
  listeners.add(listener);
1273
1276
  return () => listeners.delete(listener);
@@ -1350,9 +1353,157 @@ function createMapLifecycle(create, retire) {
1350
1353
  });
1351
1354
  }
1352
1355
 
1356
+ // src/native-boundary-feature-state.tsx
1357
+ import { VectorSource } from "@maplibre/maplibre-react-native";
1358
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from "react";
1359
+ import {
1360
+ createBoundaryFeatureState,
1361
+ tileflowBoundaryFamilies
1362
+ } from "@tileflow/core/native";
1363
+
1364
+ // src/native-boundary-feature-state-owner.ts
1365
+ var retryDelaysMs = [16, 50, 150, 400, 1e3];
1366
+ var timer = (run, delayMs) => {
1367
+ const handle = setTimeout(run, delayMs);
1368
+ return () => clearTimeout(handle);
1369
+ };
1370
+ function createNativeBoundaryFeatureStateOwner(sourceId, schedule = timer) {
1371
+ const listeners = /* @__PURE__ */ new Set();
1372
+ let source = null;
1373
+ let style = {};
1374
+ let loaded = false;
1375
+ let replacing = false;
1376
+ const present = () => !replacing && loaded && source !== null && declaredVectorSource(style, sourceId);
1377
+ let notified = false;
1378
+ const notify = () => {
1379
+ const next = present();
1380
+ if (next === notified) return;
1381
+ notified = next;
1382
+ for (const listener of [...listeners]) listener();
1383
+ };
1384
+ const absentThenNotify = () => {
1385
+ replacing = true;
1386
+ notify();
1387
+ replacing = false;
1388
+ notify();
1389
+ };
1390
+ let attempt = 0;
1391
+ let cancelRetry;
1392
+ const reset = () => {
1393
+ cancelRetry?.();
1394
+ cancelRetry = void 0;
1395
+ attempt = 0;
1396
+ };
1397
+ const rejected = (target) => {
1398
+ if (cancelRetry || target !== source || !present() || attempt >= retryDelaysMs.length) return;
1399
+ cancelRetry = schedule(() => {
1400
+ cancelRetry = void 0;
1401
+ if (target === source && present()) absentThenNotify();
1402
+ }, retryDelaysMs[attempt++]);
1403
+ };
1404
+ const write = (operation) => {
1405
+ const target = source;
1406
+ if (!target) return;
1407
+ void operation(target).then(
1408
+ () => {
1409
+ if (target === source && !cancelRetry) attempt = 0;
1410
+ },
1411
+ () => rejected(target)
1412
+ );
1413
+ };
1414
+ const map = {
1415
+ getSource: (id) => id === sourceId && present() ? {} : void 0,
1416
+ getStyle: () => style,
1417
+ on: (_type, listener) => listeners.add(listener),
1418
+ off: (_type, listener) => listeners.delete(listener),
1419
+ setFeatureState({ id, sourceLayer }, state) {
1420
+ if (!sourceLayer) return;
1421
+ write((target) => target.setFeatureState({ id, sourceLayer }, state));
1422
+ },
1423
+ removeFeatureState({ id, sourceLayer }, key2) {
1424
+ if (!sourceLayer || id === void 0) return;
1425
+ write((target) => target.removeFeatureState({ id, sourceLayer }, key2));
1426
+ }
1427
+ };
1428
+ return Object.freeze({
1429
+ map,
1430
+ /** A newly committed style is loading until MapLibre reports it loaded. */
1431
+ setStyle(next, nextLoaded) {
1432
+ if (next !== style) {
1433
+ reset();
1434
+ replacing = true;
1435
+ notify();
1436
+ replacing = false;
1437
+ }
1438
+ style = next;
1439
+ loaded = nextLoaded;
1440
+ notify();
1441
+ },
1442
+ setSource(next) {
1443
+ if (next !== source) reset();
1444
+ source = next;
1445
+ notify();
1446
+ }
1447
+ });
1448
+ }
1449
+ function declaredVectorSource(style, id) {
1450
+ const value = style.sources?.[id];
1451
+ return value?.type === "vector";
1452
+ }
1453
+
1454
+ // src/native-boundary-feature-state.tsx
1455
+ import { jsx } from "react/jsx-runtime";
1456
+ function NativeBoundaryFeatureState({
1457
+ input,
1458
+ style,
1459
+ loaded
1460
+ }) {
1461
+ const family = input.family ?? "boundaries";
1462
+ const sourceId = Object.hasOwn(tileflowBoundaryFamilies, family) ? tileflowBoundaryFamilies[family].sourceId : family;
1463
+ const owner = useMemo(() => createNativeBoundaryFeatureStateOwner(sourceId), [sourceId]);
1464
+ const [controller, setController] = useState(null);
1465
+ const [adopted, setAdopted] = useState(false);
1466
+ const bind2 = useCallback(
1467
+ (node) => {
1468
+ owner.setSource(node);
1469
+ setAdopted(node !== null);
1470
+ },
1471
+ [owner]
1472
+ );
1473
+ useLayoutEffect(() => owner.setStyle(style, loaded), [owner, style, loaded]);
1474
+ const { labels, state, values } = input;
1475
+ useEffect(() => {
1476
+ const created = createBoundaryFeatureState(owner.map, {
1477
+ family,
1478
+ ...labels === void 0 ? {} : { labels },
1479
+ ...state === void 0 ? {} : { state }
1480
+ });
1481
+ setController(created);
1482
+ return () => {
1483
+ setController(null);
1484
+ created.dispose();
1485
+ };
1486
+ }, [owner, family, labels, state]);
1487
+ useEffect(() => {
1488
+ if (!controller) return;
1489
+ if (values) controller.set(values);
1490
+ else controller.clear();
1491
+ }, [controller, values]);
1492
+ if (!declaredVectorSource(style, sourceId) || !loaded && !adopted) return null;
1493
+ const tiles = (style.sources?.[sourceId]).tiles;
1494
+ return /* @__PURE__ */ jsx(
1495
+ VectorSource,
1496
+ {
1497
+ id: sourceId,
1498
+ ref: bind2,
1499
+ ...Array.isArray(tiles) ? { tiles } : {}
1500
+ }
1501
+ );
1502
+ }
1503
+
1353
1504
  // src/native-marker.tsx
1354
1505
  import { Marker as NativeMarker } from "@maplibre/maplibre-react-native";
1355
- import { useLayoutEffect, useMemo } from "react";
1506
+ import { useLayoutEffect as useLayoutEffect2, useMemo as useMemo2 } from "react";
1356
1507
  import { Pressable, processColor, View } from "react-native";
1357
1508
 
1358
1509
  // src/native-marker-model.ts
@@ -1399,7 +1550,7 @@ function resolveNativeMarkerContent(context, render) {
1399
1550
  }
1400
1551
 
1401
1552
  // src/native-marker.tsx
1402
- import { jsx } from "react/jsx-runtime";
1553
+ import { jsx as jsx2 } from "react/jsx-runtime";
1403
1554
  var button = Object.freeze({
1404
1555
  minWidth: 44,
1405
1556
  minHeight: 44,
@@ -1421,7 +1572,7 @@ function NativeAnnotationMarker({
1421
1572
  lifecycle,
1422
1573
  renderMarker
1423
1574
  }) {
1424
- const model = useMemo(() => nativeMarkerModel(annotation, enabled), [annotation, enabled]);
1575
+ const model = useMemo2(() => nativeMarkerModel(annotation, enabled), [annotation, enabled]);
1425
1576
  const rendered = resolveNativeMarkerContent(model.context, renderMarker);
1426
1577
  let color = defaultColor;
1427
1578
  let invalidColor = false;
@@ -1435,14 +1586,14 @@ function NativeAnnotationMarker({
1435
1586
  }
1436
1587
  }
1437
1588
  const diagnostic = rendered.diagnostic ?? (!rendered.content && invalidColor ? "INVALID_FIELD" : void 0);
1438
- useLayoutEffect(() => {
1589
+ useLayoutEffect2(() => {
1439
1590
  if (diagnostic) lifecycle.interactionDiagnostic(sceneKey, diagnostic);
1440
1591
  }, [annotation, diagnostic, lifecycle, sceneKey]);
1441
1592
  const press = (event) => {
1442
1593
  event?.stopPropagation();
1443
1594
  lifecycle.markerPress(sceneKey, annotation);
1444
1595
  };
1445
- return /* @__PURE__ */ jsx(
1596
+ return /* @__PURE__ */ jsx2(
1446
1597
  NativeMarker,
1447
1598
  {
1448
1599
  id: `tileflow-annotation-${model.key}`,
@@ -1453,7 +1604,7 @@ function NativeAnnotationMarker({
1453
1604
  lifecycle.beginTouch(sceneKey);
1454
1605
  press();
1455
1606
  },
1456
- children: /* @__PURE__ */ jsx(
1607
+ children: /* @__PURE__ */ jsx2(
1457
1608
  Pressable,
1458
1609
  {
1459
1610
  accessible: false,
@@ -1464,14 +1615,14 @@ function NativeAnnotationMarker({
1464
1615
  event.stopPropagation();
1465
1616
  },
1466
1617
  onPress: press,
1467
- children: /* @__PURE__ */ jsx(
1618
+ children: /* @__PURE__ */ jsx2(
1468
1619
  View,
1469
1620
  {
1470
1621
  pointerEvents: "none",
1471
1622
  accessible: false,
1472
1623
  accessibilityElementsHidden: true,
1473
1624
  importantForAccessibility: "no-hide-descendants",
1474
- children: rendered.content ?? /* @__PURE__ */ jsx(View, { style: [dot, { backgroundColor: color }] })
1625
+ children: rendered.content ?? /* @__PURE__ */ jsx2(View, { style: [dot, { backgroundColor: color }] })
1475
1626
  }
1476
1627
  )
1477
1628
  }
@@ -1809,6 +1960,13 @@ var REFRESH_MARGIN_MS = 3e4;
1809
1960
  var ROTATION_AGE_MS = 6 * 60 * 60 * 1e3;
1810
1961
  var ROTATION_REQUEST_LIMIT = 1e4;
1811
1962
  var FUTURE_ISSUE_SKEW_MS = 3e4;
1963
+ var RETRY_INITIAL_MS = 1e3;
1964
+ var RETRY_LIMIT_MS = 6e4;
1965
+ var RETRY_AFTER_LIMIT_MS = 15 * 60 * 1e3;
1966
+ var RETRY_AFTER_SECONDS = /^\d{1,10}$/u;
1967
+ var QUOTA_CODES = /* @__PURE__ */ new Set(["COMMERCIAL_MAP_LIMIT_REACHED", "COMMERCIAL_SPENDING_LIMIT_REACHED"]);
1968
+ var SERVICE_CODE = /^[A-Z][A-Z0-9_]{0,63}$/u;
1969
+ var SERVICE_CODE_BODY_LIMIT = 4096;
1812
1970
  var MOBILE_CREDENTIAL = /^tf_public_[0-9a-f]{48}$/u;
1813
1971
  var MAP_ID = /^map_[A-Za-z0-9_-]{16}$/u;
1814
1972
  var CREDENTIAL_ID = /^key_[A-Za-z0-9_-]{8,80}$/u;
@@ -1847,6 +2005,14 @@ var SUCCESS_KEYS = /* @__PURE__ */ new Set([
1847
2005
  "tilesetIds"
1848
2006
  ]);
1849
2007
  var RESTART_KEYS = /* @__PURE__ */ new Set(["code", "error", "retryWithNewSession", "sessionId"]);
2008
+ var TERMINAL_CODES = /* @__PURE__ */ new Set([
2009
+ "NATIVE_SESSION_INPUT_INVALID",
2010
+ "NATIVE_SESSION_CREDENTIAL_REJECTED",
2011
+ "NATIVE_SESSION_MAP_MISMATCH",
2012
+ "NATIVE_SESSION_MAP_NOT_FOUND",
2013
+ "NATIVE_SESSION_QUOTA_EXCEEDED",
2014
+ "NATIVE_SESSION_REJECTED"
2015
+ ]);
1850
2016
  var ERROR_MESSAGES = {
1851
2017
  NATIVE_SESSION_INPUT_INVALID: "Native session configuration is invalid.",
1852
2018
  NATIVE_SESSION_RESPONSE_TOO_LARGE: "Native session response exceeded its byte limit.",
@@ -1858,18 +2024,22 @@ var ERROR_MESSAGES = {
1858
2024
  NATIVE_SESSION_MAP_NOT_FOUND: "Native session Map was not found.",
1859
2025
  NATIVE_SESSION_QUOTA_EXCEEDED: "Native session quota was exceeded.",
1860
2026
  NATIVE_SESSION_REJECTED: "Native session request was rejected.",
2027
+ NATIVE_SESSION_RATE_LIMITED: "Native session requests are rate limited.",
1861
2028
  NATIVE_SESSION_UNAVAILABLE: "Native session service is unavailable.",
1862
2029
  NATIVE_SESSION_REPLACED: "Native session binding was replaced.",
1863
2030
  NATIVE_SESSION_DISPOSED: "Native session controller is disposed."
1864
2031
  };
1865
2032
  var HostedNativeSessionError = class extends Error {
1866
- constructor(code) {
2033
+ constructor(code, reason) {
1867
2034
  super(ERROR_MESSAGES[code]);
1868
2035
  __publicField(this, "code");
1869
2036
  __publicField(this, "kind");
1870
2037
  this.name = "HostedNativeSessionError";
1871
2038
  this.code = code;
1872
2039
  this.kind = errorKind(code);
2040
+ if (code === "NATIVE_SESSION_REJECTED" && typeof reason === "string" && SERVICE_CODE.test(reason)) {
2041
+ this.reason = reason;
2042
+ }
1873
2043
  }
1874
2044
  };
1875
2045
  function createHostedNativeSessionController(input) {
@@ -1979,7 +2149,29 @@ function createHostedNativeSessionController(input) {
1979
2149
  })
1980
2150
  );
1981
2151
  }
2152
+ let terminal = null;
2153
+ let failures = 0;
2154
+ let retryAt = null;
2155
+ let retryError = null;
2156
+ function clearRetry() {
2157
+ failures = 0;
2158
+ retryAt = null;
2159
+ retryError = null;
2160
+ }
2161
+ function delayRetry(error) {
2162
+ let at;
2163
+ try {
2164
+ at = readClock();
2165
+ } catch {
2166
+ return;
2167
+ }
2168
+ if (retryAt === null || at >= retryAt) failures = Math.min(failures + 1, 16);
2169
+ const backoff = Math.min(RETRY_INITIAL_MS * 2 ** (failures - 1), RETRY_LIMIT_MS);
2170
+ retryAt = Math.max(retryAt ?? at, at + Math.max(backoff, retryAfterDelays.get(error) ?? 0));
2171
+ retryError = safeError(error);
2172
+ }
1982
2173
  function rotateIfRequired(at) {
2174
+ if (terminal) throw errorFrom(terminal);
1983
2175
  if (!activeSession || binding.kind !== "hosted") return;
1984
2176
  if (at - activeSession.startedAt < ROTATION_AGE_MS && activeSession.requestCount < ROTATION_REQUEST_LIMIT) {
1985
2177
  return;
@@ -2013,6 +2205,7 @@ function createHostedNativeSessionController(input) {
2013
2205
  return current;
2014
2206
  }
2015
2207
  async function ensureAuthority(session, at) {
2208
+ if (terminal) throw errorFrom(terminal);
2016
2209
  if (session.authority && session.authority.clockEpoch === clock.epoch && session.authority.validUntil - at > REFRESH_MARGIN_MS) {
2017
2210
  return { authority: session.authority, session };
2018
2211
  }
@@ -2020,20 +2213,28 @@ function createHostedNativeSessionController(input) {
2020
2213
  if (binding.kind !== "hosted") {
2021
2214
  throw new HostedNativeSessionError("NATIVE_SESSION_REPLACED");
2022
2215
  }
2216
+ if (retryAt !== null && retryError && at < retryAt) {
2217
+ if (session.authority && session.authority.clockEpoch === clock.epoch && session.authority.validUntil > at) {
2218
+ return { authority: session.authority, session };
2219
+ }
2220
+ throw errorFrom(retryError);
2221
+ }
2023
2222
  const operationBinding = binding;
2024
2223
  const abort = new SessionAbortController();
2025
2224
  operations.add(abort);
2026
2225
  const operation = {
2027
2226
  abort,
2028
2227
  promise: Promise.resolve().then(async () => {
2029
- session.error = null;
2030
- publishSession(session, "loading");
2031
- if (abort.signal.aborted) throw abort.error();
2032
2228
  try {
2229
+ if (abort.signal.aborted) throw abort.error();
2230
+ session.error = null;
2231
+ publishSession(session, "loading");
2232
+ if (abort.signal.aborted) throw abort.error();
2033
2233
  const result = await bootstrap(session, operationBinding, abort, true, true);
2034
2234
  if (abort.signal.aborted) throw abort.error();
2035
2235
  result.session.authority = result.authority;
2036
2236
  result.session.error = null;
2237
+ if (binding === operationBinding) clearRetry();
2037
2238
  if (activeSession === result.session) {
2038
2239
  publishSession(result.session, "ready");
2039
2240
  if (abort.signal.aborted) throw abort.error();
@@ -2042,8 +2243,26 @@ function createHostedNativeSessionController(input) {
2042
2243
  } catch (error) {
2043
2244
  const normalized = abort.signal.aborted ? abort.error() : normalizeError(error);
2044
2245
  const target = latestSession(session);
2246
+ if (normalized.kind === "terminal" && binding === operationBinding && !terminal) {
2247
+ const failure3 = safeError(normalized);
2248
+ terminal = failure3;
2249
+ const rejectedSession = activeSession;
2250
+ if (rejectedSession) {
2251
+ rejectedSession.authority = null;
2252
+ rejectedSession.error = failure3;
2253
+ }
2254
+ for (const pending of [...operations]) {
2255
+ pending.abort(failure3.code, failure3.reason);
2256
+ }
2257
+ if (binding === operationBinding && rejectedSession) {
2258
+ publishSession(rejectedSession, "error", failure3);
2259
+ }
2260
+ }
2261
+ if (normalized.kind === "recoverable" && binding === operationBinding && !terminal) {
2262
+ delayRetry(normalized);
2263
+ }
2045
2264
  if (normalized.code !== "NATIVE_SESSION_REPLACED" && normalized.code !== "NATIVE_SESSION_DISPOSED") {
2046
- target.error = Object.freeze({ code: normalized.code, kind: normalized.kind });
2265
+ target.error = safeError(normalized);
2047
2266
  publishSession(target, "error", target.error);
2048
2267
  }
2049
2268
  throw normalized;
@@ -2069,20 +2288,29 @@ function createHostedNativeSessionController(input) {
2069
2288
  if (abort.signal.aborted) throw abort.error();
2070
2289
  const expectedSurfaceId = session.authority?.public.surfaceId ?? session.requestedSurfaceId;
2071
2290
  const response = await fetchResponse(hostedBinding, session, expectedSurfaceId, abort);
2072
- const source = await readBoundedResponse(response, abort);
2291
+ const { source, size } = await readBoundedResponse(response, abort);
2073
2292
  let body;
2074
2293
  try {
2075
2294
  body = source.length ? JSON.parse(source) : null;
2076
2295
  } catch {
2077
- throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_JSON_INVALID");
2296
+ if (response.status === 201) {
2297
+ throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_JSON_INVALID");
2298
+ }
2299
+ body = null;
2078
2300
  }
2079
- if (response.status === 409 && allowRestart && isExactRestart(body, session.sessionId)) {
2301
+ if (response.status === 409 && isExactRestart(body, session.sessionId)) {
2302
+ if (!allowRestart) throw new HostedNativeSessionError("NATIVE_SESSION_UNAVAILABLE");
2080
2303
  assertCurrentSession(session, hostedBinding);
2081
2304
  const replacement = redirectSession(session, readClock());
2082
2305
  if (abort.signal.aborted) throw abort.error();
2083
2306
  return bootstrap(replacement, hostedBinding, abort, false, false);
2084
2307
  }
2085
- if (response.status !== 201) throw statusError(response.status);
2308
+ if (response.status !== 201) {
2309
+ const failure3 = statusError(response.status, body, size);
2310
+ const delay = retryAfterMs(response.headers, now);
2311
+ if (delay !== null) retryAfterDelays.set(failure3, delay);
2312
+ throw failure3;
2313
+ }
2086
2314
  if (!hasNoStore(response.headers.get("cache-control"))) {
2087
2315
  throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
2088
2316
  }
@@ -2155,6 +2383,16 @@ function createHostedNativeSessionController(input) {
2155
2383
  if (binding.kind === "direct" || !activeSession) {
2156
2384
  return Object.freeze({ status: "idle", source: "direct", lifecycle });
2157
2385
  }
2386
+ if (terminal) {
2387
+ return Object.freeze({
2388
+ status: "error",
2389
+ source: "hosted",
2390
+ lifecycle,
2391
+ mapId: binding.mapId,
2392
+ sessionId: activeSession.sessionId,
2393
+ error: terminal
2394
+ });
2395
+ }
2158
2396
  if (activeSession.operation) {
2159
2397
  return Object.freeze({
2160
2398
  status: "loading",
@@ -2206,6 +2444,7 @@ function createHostedNativeSessionController(input) {
2206
2444
  if (bindingGeneration !== expectedGeneration) {
2207
2445
  throw new HostedNativeSessionError("NATIVE_SESSION_REPLACED");
2208
2446
  }
2447
+ if (terminal) throw errorFrom(terminal);
2209
2448
  checkedAt = readClock();
2210
2449
  if (resolved.authority.clockEpoch === clock.epoch && resolved.authority.validUntil > checkedAt) {
2211
2450
  admittedAuthorities.set(resolved.authority.public, resolved.authority);
@@ -2260,7 +2499,7 @@ function createHostedNativeSessionController(input) {
2260
2499
  }
2261
2500
  },
2262
2501
  transportBudget(authority) {
2263
- if (disposed || lifecycle !== "foreground") return 0;
2502
+ if (disposed || terminal || lifecycle !== "foreground") return 0;
2264
2503
  const stored = admittedAuthorities.get(authority);
2265
2504
  if (!stored || stored.bindingGeneration !== bindingGeneration) return 0;
2266
2505
  try {
@@ -2279,6 +2518,11 @@ function createHostedNativeSessionController(input) {
2279
2518
  async resume() {
2280
2519
  if (disposed) throw new HostedNativeSessionError("NATIVE_SESSION_DISPOSED");
2281
2520
  lifecycle = "foreground";
2521
+ if (terminal) {
2522
+ const error = errorFrom(terminal);
2523
+ publish(currentSnapshot());
2524
+ throw error;
2525
+ }
2282
2526
  if (binding.kind === "direct" || !activeSession) {
2283
2527
  publish(currentSnapshot());
2284
2528
  return;
@@ -2301,9 +2545,27 @@ function createHostedNativeSessionController(input) {
2301
2545
  bindingGeneration += 1;
2302
2546
  for (const operation of operations) operation.abort("NATIVE_SESSION_REPLACED");
2303
2547
  binding = next;
2548
+ terminal = null;
2549
+ clearRetry();
2304
2550
  activeSession = next.kind === "hosted" ? createSession(next.surfaceId, readClock()) : null;
2305
2551
  publish(snapshotIdle(binding, activeSession, lifecycle));
2306
2552
  },
2553
+ retry() {
2554
+ if (disposed) throw new HostedNativeSessionError("NATIVE_SESSION_DISPOSED");
2555
+ if (!terminal && retryAt === null) return;
2556
+ terminal = null;
2557
+ clearRetry();
2558
+ if (activeSession && !activeSession.operation) activeSession.error = null;
2559
+ publish(currentSnapshot());
2560
+ },
2561
+ retryDelay() {
2562
+ if (disposed || terminal || retryAt === null) return null;
2563
+ try {
2564
+ return Math.max(0, Math.ceil(retryAt - readClock()));
2565
+ } catch {
2566
+ return null;
2567
+ }
2568
+ },
2307
2569
  subscribe(listener) {
2308
2570
  if (typeof listener !== "function") {
2309
2571
  throw new HostedNativeSessionError("NATIVE_SESSION_INPUT_INVALID");
@@ -2398,7 +2660,8 @@ function parseSuccess(value, binding, expectedSessionId, expectedSurfaceId) {
2398
2660
  const issuedAtMs = parseExactIso(value.issuedAt);
2399
2661
  const serverTimeMs = parseExactIso(value.serverTime);
2400
2662
  const expiresAtMs = parseExactIso(value.expiresAt);
2401
- if (issuedAtMs > serverTimeMs + FUTURE_ISSUE_SKEW_MS || expiresAtMs <= serverTimeMs || expiresAtMs <= issuedAtMs || expiresAtMs - issuedAtMs > MAX_GRANT_LIFETIME_MS || expiresAtMs - issuedAtMs > MAX_GRANT_LIFETIME_MS) {
2663
+ if (issuedAtMs > serverTimeMs + FUTURE_ISSUE_SKEW_MS || expiresAtMs <= serverTimeMs || expiresAtMs <= issuedAtMs || expiresAtMs - issuedAtMs > MAX_GRANT_LIFETIME_MS || // The local deadline is derived from serverTime, so this also bounds the transport budget.
2664
+ expiresAtMs - serverTimeMs > MAX_GRANT_LIFETIME_MS) {
2402
2665
  throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
2403
2666
  }
2404
2667
  const authority = {
@@ -2529,7 +2792,7 @@ async function readBoundedResponse(response, abort) {
2529
2792
  bytes.set(chunk, offset);
2530
2793
  offset += chunk.byteLength;
2531
2794
  }
2532
- return decodeUtf8(bytes);
2795
+ return { source: decodeUtf8(bytes), size };
2533
2796
  }
2534
2797
  function decodeUtf8(bytes) {
2535
2798
  let result = "";
@@ -2577,18 +2840,60 @@ function decodeUtf8(bytes) {
2577
2840
  function hasNoStore(value) {
2578
2841
  return typeof value === "string" && value.split(",").some((part) => part.trim().toLowerCase() === "no-store");
2579
2842
  }
2580
- function statusError(status) {
2843
+ function statusError(status, body, size) {
2581
2844
  if (status === 401) return new HostedNativeSessionError("NATIVE_SESSION_CREDENTIAL_REJECTED");
2582
2845
  if (status === 403) return new HostedNativeSessionError("NATIVE_SESSION_MAP_MISMATCH");
2583
2846
  if (status === 404) return new HostedNativeSessionError("NATIVE_SESSION_MAP_NOT_FOUND");
2584
- if (status === 429) return new HostedNativeSessionError("NATIVE_SESSION_QUOTA_EXCEEDED");
2585
- if (status === 503) return new HostedNativeSessionError("NATIVE_SESSION_UNAVAILABLE");
2586
- return new HostedNativeSessionError("NATIVE_SESSION_REJECTED");
2847
+ if (status === 429) {
2848
+ return new HostedNativeSessionError(
2849
+ isRecord(body) && typeof body.code === "string" && QUOTA_CODES.has(body.code) ? "NATIVE_SESSION_QUOTA_EXCEEDED" : "NATIVE_SESSION_RATE_LIMITED"
2850
+ );
2851
+ }
2852
+ if (status === 408 || status >= 500 && status <= 599) {
2853
+ return new HostedNativeSessionError("NATIVE_SESSION_UNAVAILABLE");
2854
+ }
2855
+ if (status >= 400 && status <= 499) {
2856
+ return new HostedNativeSessionError(
2857
+ "NATIVE_SESSION_REJECTED",
2858
+ size <= SERVICE_CODE_BODY_LIMIT && isRecord(body) && typeof body.code === "string" ? body.code : void 0
2859
+ );
2860
+ }
2861
+ return new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
2862
+ }
2863
+ function safeError(error) {
2864
+ return Object.freeze(
2865
+ error.reason === void 0 ? { code: error.code, kind: error.kind } : { code: error.code, kind: error.kind, reason: error.reason }
2866
+ );
2867
+ }
2868
+ function errorFrom(error) {
2869
+ return new HostedNativeSessionError(error.code, error.reason);
2870
+ }
2871
+ var retryAfterDelays = /* @__PURE__ */ new WeakMap();
2872
+ function retryAfterMs(headers, now) {
2873
+ let value;
2874
+ try {
2875
+ value = headers.get("retry-after");
2876
+ } catch {
2877
+ return null;
2878
+ }
2879
+ if (typeof value !== "string" || value.length > 64) return null;
2880
+ const text = value.trim();
2881
+ let delay;
2882
+ if (RETRY_AFTER_SECONDS.test(text)) delay = Number(text) * 1e3;
2883
+ else {
2884
+ const date = Date.parse(text);
2885
+ if (!Number.isFinite(date)) return null;
2886
+ try {
2887
+ delay = date - readWallClock(now);
2888
+ } catch {
2889
+ return null;
2890
+ }
2891
+ }
2892
+ return Math.min(Math.max(0, delay), RETRY_AFTER_LIMIT_MS);
2587
2893
  }
2588
2894
  function errorKind(code) {
2589
2895
  if (code === "NATIVE_SESSION_DISPOSED" || code === "NATIVE_SESSION_REPLACED") return "cancelled";
2590
- if (code === "NATIVE_SESSION_UNAVAILABLE") return "recoverable";
2591
- return "terminal";
2896
+ return TERMINAL_CODES.has(code) ? "terminal" : "recoverable";
2592
2897
  }
2593
2898
  function normalizeError(error, fallback = "NATIVE_SESSION_RESPONSE_INVALID") {
2594
2899
  return error instanceof HostedNativeSessionError ? error : new HostedNativeSessionError(fallback);
@@ -2611,12 +2916,13 @@ async function cancelResponse(response) {
2611
2916
  } catch {
2612
2917
  }
2613
2918
  }
2614
- var _aborted, _code, _listeners;
2919
+ var _aborted, _code, _reason, _listeners;
2615
2920
  var SessionAbortController = class {
2616
2921
  constructor() {
2617
2922
  __publicField(this, "signal");
2618
2923
  __privateAdd(this, _aborted, false);
2619
2924
  __privateAdd(this, _code, "NATIVE_SESSION_REPLACED");
2925
+ __privateAdd(this, _reason);
2620
2926
  __privateAdd(this, _listeners, /* @__PURE__ */ new Set());
2621
2927
  const isAborted = () => __privateGet(this, _aborted);
2622
2928
  this.signal = Object.freeze({
@@ -2631,9 +2937,10 @@ var SessionAbortController = class {
2631
2937
  }
2632
2938
  });
2633
2939
  }
2634
- abort(code) {
2940
+ abort(code, reason) {
2635
2941
  if (__privateGet(this, _aborted)) return;
2636
2942
  __privateSet(this, _code, code);
2943
+ __privateSet(this, _reason, reason);
2637
2944
  __privateSet(this, _aborted, true);
2638
2945
  for (const listener of [...__privateGet(this, _listeners)]) {
2639
2946
  try {
@@ -2644,11 +2951,12 @@ var SessionAbortController = class {
2644
2951
  __privateGet(this, _listeners).clear();
2645
2952
  }
2646
2953
  error() {
2647
- return new HostedNativeSessionError(__privateGet(this, _code));
2954
+ return new HostedNativeSessionError(__privateGet(this, _code), __privateGet(this, _reason));
2648
2955
  }
2649
2956
  };
2650
2957
  _aborted = new WeakMap();
2651
2958
  _code = new WeakMap();
2959
+ _reason = new WeakMap();
2652
2960
  _listeners = new WeakMap();
2653
2961
  function raceWithAbort(promise, controller) {
2654
2962
  if (controller.signal.aborted) return Promise.reject(controller.error());
@@ -2887,6 +3195,8 @@ function createNativeAdmissionOwner(options) {
2887
3195
  try {
2888
3196
  resources = normalizeNativeResources(input.resources);
2889
3197
  if (input.onRetired !== void 0 && typeof input.onRetired !== "function") throw cancel();
3198
+ if (input.onSessionError !== void 0 && typeof input.onSessionError !== "function")
3199
+ throw cancel();
2890
3200
  } catch {
2891
3201
  throw cancel("NATIVE_ADMISSION_INVALID");
2892
3202
  }
@@ -2947,6 +3257,17 @@ function createNativeAdmissionOwner(options) {
2947
3257
  controller,
2948
3258
  handle: void 0
2949
3259
  };
3260
+ let reported = null;
3261
+ const releaseSession = controller.subscribe((state) => {
3262
+ const code = state.status === "error" ? state.error.code : null;
3263
+ if (!context.live || code === reported) return;
3264
+ reported = code;
3265
+ if (state.status !== "error") return;
3266
+ try {
3267
+ input.onSessionError?.(Object.freeze({ ...state.error }));
3268
+ } catch {
3269
+ }
3270
+ });
2950
3271
  context.handle = Object.freeze({
2951
3272
  context: context.id,
2952
3273
  generation: context.generation,
@@ -2976,9 +3297,26 @@ function createNativeAdmissionOwner(options) {
2976
3297
  discriminate: catalog.discriminate,
2977
3298
  discriminateForTest: catalog.discriminate,
2978
3299
  extendResources: catalog.extend,
3300
+ retry() {
3301
+ if (!context.live || disposed) return;
3302
+ try {
3303
+ controller.retry?.();
3304
+ } catch {
3305
+ }
3306
+ },
3307
+ retryDelay() {
3308
+ if (!context.live || disposed) return null;
3309
+ try {
3310
+ const delay = controller.retryDelay?.() ?? null;
3311
+ return delay !== null && Number.isSafeInteger(delay) && delay >= 0 ? delay : null;
3312
+ } catch {
3313
+ return null;
3314
+ }
3315
+ },
2979
3316
  retire() {
2980
3317
  if (retirement) return retirement;
2981
3318
  context.live = false;
3319
+ releaseSession();
2982
3320
  context.batch = null;
2983
3321
  context.tickets.clear();
2984
3322
  catalog.retire();
@@ -5502,6 +5840,20 @@ function themeBlendKey(blend) {
5502
5840
  }
5503
5841
 
5504
5842
  // src/mounted-map-owner.ts
5843
+ var timer2 = (run, delayMs) => {
5844
+ const handle = setTimeout(run, delayMs);
5845
+ return () => clearTimeout(handle);
5846
+ };
5847
+ var REOPEN_LIMIT = 3;
5848
+ var sessionErrorCodes = /* @__PURE__ */ new Set([
5849
+ "NATIVE_SESSION_CREDENTIAL_REJECTED",
5850
+ "NATIVE_SESSION_MAP_MISMATCH",
5851
+ "NATIVE_SESSION_MAP_NOT_FOUND",
5852
+ "NATIVE_SESSION_QUOTA_EXCEEDED",
5853
+ "NATIVE_SESSION_REJECTED",
5854
+ "NATIVE_SESSION_INPUT_INVALID"
5855
+ ]);
5856
+ var SERVICE_CODE2 = /^[A-Z][A-Z0-9_]{0,63}$/u;
5505
5857
  function themeSource(source, name3) {
5506
5858
  const theme = Object.hasOwn(source.map.themes, name3) ? source.map.themes[name3] : void 0;
5507
5859
  if (!theme) throw new NativePreparationError();
@@ -5584,7 +5936,9 @@ function createMountedMapOwner(ports2) {
5584
5936
  let sourceState;
5585
5937
  let current;
5586
5938
  let lastReadiness = "";
5939
+ let reopenings = 0;
5587
5940
  let disposal;
5941
+ const schedule = ports2.schedule ?? timer2;
5588
5942
  const retired = /* @__PURE__ */ new Set();
5589
5943
  const tasks = /* @__PURE__ */ new Set();
5590
5944
  const listeners = /* @__PURE__ */ new Set();
@@ -5619,10 +5973,11 @@ function createMountedMapOwner(ports2) {
5619
5973
  if (lastReadiness === identity) return;
5620
5974
  lastReadiness = identity;
5621
5975
  }
5976
+ if (event.type === "load") reopenings = 0;
5622
5977
  try {
5623
5978
  let result;
5624
5979
  if (event.type === "load") result = props.onLoad?.(event);
5625
- else if (event.type === "renderer-error" || event.type === "source-error")
5980
+ else if (event.type === "renderer-error" || event.type === "source-error" || event.type === "session-error")
5626
5981
  result = props.onError?.(event);
5627
5982
  else if (event.type === "readiness-change") result = props.onReadinessChange?.(event);
5628
5983
  else result = props.onThemeChange?.(event);
@@ -5699,11 +6054,29 @@ function createMountedMapOwner(ports2) {
5699
6054
  }
5700
6055
  }
5701
6056
  }
6057
+ function cancelRetry(epoch) {
6058
+ const cancel = epoch.retry;
6059
+ epoch.retry = void 0;
6060
+ cancel?.();
6061
+ }
6062
+ function retryLater(epoch, source) {
6063
+ const delay = epoch.map?.retryDelay();
6064
+ if (typeof delay !== "number") return;
6065
+ cancelRetry(epoch);
6066
+ const cancel = schedule(() => {
6067
+ if (epoch.retry !== cancel) return;
6068
+ epoch.retry = void 0;
6069
+ if (!disposed && foreground && current === epoch && epoch.live && core.state === source)
6070
+ prepare(epoch, source);
6071
+ }, delay);
6072
+ epoch.retry = cancel;
6073
+ }
5702
6074
  function retire(epoch) {
5703
6075
  if (epoch.retirement) return epoch.retirement;
5704
6076
  epoch.live = false;
5705
6077
  retired.add(epoch);
5706
6078
  cancelJob(epoch);
6079
+ cancelRetry(epoch);
5707
6080
  epoch.cache.clear();
5708
6081
  epoch.binding.dispose();
5709
6082
  const context2 = epoch.map?.retire();
@@ -5727,6 +6100,29 @@ function createMountedMapOwner(ports2) {
5727
6100
  });
5728
6101
  return track(attempt);
5729
6102
  }
6103
+ function restart(epoch) {
6104
+ current = void 0;
6105
+ void retire(epoch).catch(() => void 0);
6106
+ current = createEpoch(epoch.developmentOrigin);
6107
+ select();
6108
+ }
6109
+ function reopen(epoch) {
6110
+ if (disposed || current !== epoch || !epoch.live || !props) return;
6111
+ rendererError();
6112
+ if (reopenings >= REOPEN_LIMIT) return;
6113
+ reopenings += 1;
6114
+ restart(epoch);
6115
+ }
6116
+ function sessionError(epoch, code, reason) {
6117
+ if (disposed || current !== epoch || !epoch.live || !sessionErrorCodes.has(code)) return;
6118
+ const detail = code === "NATIVE_SESSION_REJECTED" && typeof reason === "string" && SERVICE_CODE2.test(reason) ? { reason } : {};
6119
+ const error = Object.freeze({
6120
+ code,
6121
+ kind: "terminal",
6122
+ ...detail
6123
+ });
6124
+ emit({ type: "session-error", generation: generation(), error });
6125
+ }
5730
6126
  function createEpoch(developmentOrigin) {
5731
6127
  const epoch = {
5732
6128
  live: true,
@@ -5754,7 +6150,17 @@ function createMountedMapOwner(ports2) {
5754
6150
  if (!epoch.live || current !== epoch) throw new NativePreparationError();
5755
6151
  const binding = await epoch.binding.replace(source);
5756
6152
  if (!epoch.live || current !== epoch) throw new NativePreparationError();
5757
- epoch.lease = ports2.installation.open({ binding, resources: [], now: ports2.now });
6153
+ epoch.lease = ports2.installation.open({
6154
+ binding,
6155
+ resources: [],
6156
+ now: ports2.now,
6157
+ // Admission reports from inside its own cleanup and session publication; defer.
6158
+ onRetired: () => void Promise.resolve().then(() => reopen(epoch)),
6159
+ onSessionError: (error) => {
6160
+ if (error.kind !== "terminal") return;
6161
+ void Promise.resolve().then(() => sessionError(epoch, error.code, error.reason));
6162
+ }
6163
+ });
5758
6164
  const map = await epoch.lease.ready;
5759
6165
  epoch.map = map;
5760
6166
  if (!epoch.live || current !== epoch || map.state.status !== "active") {
@@ -5763,7 +6169,9 @@ function createMountedMapOwner(ports2) {
5763
6169
  }
5764
6170
  return map;
5765
6171
  });
5766
- void epoch.context.catch(() => void 0);
6172
+ void epoch.context.catch(() => {
6173
+ epoch.contextFailed = true;
6174
+ });
5767
6175
  }
5768
6176
  return epoch.context;
5769
6177
  }
@@ -5905,6 +6313,7 @@ function createMountedMapOwner(ports2) {
5905
6313
  }
5906
6314
  rendererError();
5907
6315
  epoch.renderer?.preparationFailed(source.generation);
6316
+ retryLater(epoch, source);
5908
6317
  }).finally(() => {
5909
6318
  if (epoch.job === job) epoch.job = void 0;
5910
6319
  });
@@ -5962,6 +6371,7 @@ function createMountedMapOwner(ports2) {
5962
6371
  foreground = false;
5963
6372
  if (current) {
5964
6373
  cancelJob(current);
6374
+ cancelRetry(current);
5965
6375
  current.renderer?.background();
5966
6376
  }
5967
6377
  return;
@@ -6060,6 +6470,22 @@ function createMountedMapOwner(ports2) {
6060
6470
  if (state?.status !== "ready") select();
6061
6471
  },
6062
6472
  nativeLifecycle,
6473
+ /** Loads again after a stopped session, a failed source or a failed context. */
6474
+ retry() {
6475
+ const epoch = current;
6476
+ if (disposed || !props || !epoch?.live) return;
6477
+ reopenings = 0;
6478
+ const map = epoch.map;
6479
+ if (epoch.contextFailed || map?.state.status === "retired") {
6480
+ restart(epoch);
6481
+ return;
6482
+ }
6483
+ map?.retry();
6484
+ cancelRetry(epoch);
6485
+ const state = core.state;
6486
+ if (state?.status === "error") select();
6487
+ else if (state?.status === "ready" && map && foreground) prepare(epoch, state);
6488
+ },
6063
6489
  async whenIdle() {
6064
6490
  while (tasks.size) await Promise.allSettled([...tasks]);
6065
6491
  await current?.renderer?.whenIdle();
@@ -6097,7 +6523,9 @@ var nativeBootstrapLimits = Object.freeze({
6097
6523
  queueDepth: 32,
6098
6524
  requestBytes: 2048,
6099
6525
  responseBytes: 65536,
6100
- timeoutMs: 3e4
6526
+ retryAfterCharacters: 64,
6527
+ // Well inside the 30 s ticket and batch deadlines.
6528
+ timeoutMs: 1e4
6101
6529
  });
6102
6530
 
6103
6531
  // src/native-admission-wire.ts
@@ -6128,10 +6556,11 @@ function decodeBoundedBase64(value) {
6128
6556
  return bytes;
6129
6557
  }
6130
6558
  function responseFromWire(reply, isAborted, release) {
6131
- if (!reply || !Number.isInteger(reply.status) || reply.status < 100 || reply.status > 599 || typeof reply.cacheControl !== "string" || reply.cacheControl.length > 1024)
6559
+ if (!reply || !Number.isInteger(reply.status) || reply.status < 100 || reply.status > 599 || typeof reply.cacheControl !== "string" || reply.cacheControl.length > 1024 || reply.retryAfter !== void 0 && (typeof reply.retryAfter !== "string" || reply.retryAfter.length > nativeBootstrapLimits.retryAfterCharacters))
6132
6560
  throw invalid4();
6133
6561
  const status = reply.status;
6134
6562
  const cacheControl = reply.cacheControl;
6563
+ const retryAfter = reply.retryAfter || null;
6135
6564
  let bytes = decodeBoundedBase64(reply.bodyBase64);
6136
6565
  let readerTaken = false;
6137
6566
  let done = false;
@@ -6145,7 +6574,9 @@ function responseFromWire(reply, isAborted, release) {
6145
6574
  status,
6146
6575
  headers: Object.freeze({
6147
6576
  get(name3) {
6148
- return name3.toLowerCase() === "cache-control" ? cacheControl : null;
6577
+ const header = name3.toLowerCase();
6578
+ if (header === "cache-control") return cacheControl;
6579
+ return header === "retry-after" ? retryAfter : null;
6149
6580
  }
6150
6581
  }),
6151
6582
  body: Object.freeze({
@@ -7272,7 +7703,7 @@ function createNativeMapOwner() {
7272
7703
  }
7273
7704
 
7274
7705
  // src/map.tsx
7275
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
7706
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
7276
7707
  var fill = Object.freeze({ flex: 1 });
7277
7708
  function NativeScene({
7278
7709
  scene,
@@ -7281,7 +7712,8 @@ function NativeScene({
7281
7712
  children,
7282
7713
  interactions,
7283
7714
  interactionEnabled,
7284
- renderMarker
7715
+ renderMarker,
7716
+ boundaryFeatureState
7285
7717
  }) {
7286
7718
  const view = useRef(null);
7287
7719
  const camera = useRef(null);
@@ -7291,7 +7723,10 @@ function NativeScene({
7291
7723
  const laidOut = useRef(false);
7292
7724
  const styleLoaded = useRef(false);
7293
7725
  const attachRequested = useRef(false);
7294
- const host = useMemo2(() => {
7726
+ const latestStyle = useRef(scene.style);
7727
+ latestStyle.current = scene.style;
7728
+ const [loadedStyle, setLoadedStyle] = useState2(void 0);
7729
+ const host = useMemo3(() => {
7295
7730
  const value = Object.freeze({
7296
7731
  key: scene.key,
7297
7732
  style: scene.style,
@@ -7305,7 +7740,7 @@ function NativeScene({
7305
7740
  });
7306
7741
  return value;
7307
7742
  }, [scene.key, scene.style]);
7308
- useLayoutEffect2(() => {
7743
+ useLayoutEffect3(() => {
7309
7744
  committedHost.current = host;
7310
7745
  lifecycle.bindInteractionHost(host);
7311
7746
  return () => {
@@ -7313,11 +7748,11 @@ function NativeScene({
7313
7748
  lifecycle.unbindInteractionHost(host);
7314
7749
  };
7315
7750
  }, [host, lifecycle]);
7316
- const mapStyle = useMemo2(
7751
+ const mapStyle = useMemo3(
7317
7752
  () => JSON.stringify(scene.style),
7318
7753
  [scene.style]
7319
7754
  );
7320
- const [initialViewState] = useState(() => {
7755
+ const [initialViewState] = useState2(() => {
7321
7756
  const center = [scene.initialView.center[0], scene.initialView.center[1]];
7322
7757
  Object.freeze(center);
7323
7758
  return Object.freeze({
@@ -7343,7 +7778,7 @@ function NativeScene({
7343
7778
  };
7344
7779
  const annotations = interactions?.annotations;
7345
7780
  const byId = new globalThis.Map(annotations?.map((annotation) => [annotation.id, annotation]));
7346
- return /* @__PURE__ */ jsx2(
7781
+ return /* @__PURE__ */ jsx3(
7347
7782
  View2,
7348
7783
  {
7349
7784
  ref: bind2,
@@ -7381,10 +7816,11 @@ function NativeScene({
7381
7816
  },
7382
7817
  onDidFinishLoadingStyle: () => {
7383
7818
  styleLoaded.current = true;
7819
+ setLoadedStyle(latestStyle.current);
7384
7820
  attach();
7385
7821
  },
7386
7822
  children: [
7387
- /* @__PURE__ */ jsx2(
7823
+ /* @__PURE__ */ jsx3(
7388
7824
  NativeCamera,
7389
7825
  {
7390
7826
  ref: camera,
@@ -7394,9 +7830,9 @@ function NativeScene({
7394
7830
  },
7395
7831
  "tileflow-camera"
7396
7832
  ),
7397
- /* @__PURE__ */ jsx2(Fragment, { children: interactions?.plan.order.map((id) => {
7833
+ /* @__PURE__ */ jsx3(Fragment, { children: interactions?.plan.order.map((id) => {
7398
7834
  const annotation = byId.get(id);
7399
- return annotation ? /* @__PURE__ */ jsx2(
7835
+ return annotation ? /* @__PURE__ */ jsx3(
7400
7836
  NativeAnnotationMarker,
7401
7837
  {
7402
7838
  annotation,
@@ -7408,6 +7844,15 @@ function NativeScene({
7408
7844
  annotation.id
7409
7845
  ) : null;
7410
7846
  }) }, "tileflow-annotations"),
7847
+ boundaryFeatureState ? /* @__PURE__ */ jsx3(
7848
+ NativeBoundaryFeatureState,
7849
+ {
7850
+ input: boundaryFeatureState,
7851
+ style: scene.style,
7852
+ loaded: loadedStyle === scene.style
7853
+ },
7854
+ "tileflow-boundary-feature-state"
7855
+ ) : null,
7411
7856
  children
7412
7857
  ]
7413
7858
  }
@@ -7416,7 +7861,7 @@ function NativeScene({
7416
7861
  );
7417
7862
  }
7418
7863
  function Map2(props) {
7419
- const [lifecycle] = useState(
7864
+ const [lifecycle] = useState2(
7420
7865
  () => createMapLifecycle(createNativeMapOwner, retireNativeMapOwner)
7421
7866
  );
7422
7867
  const snapshot = useSyncExternalStore(
@@ -7424,10 +7869,12 @@ function Map2(props) {
7424
7869
  lifecycle.getSnapshot,
7425
7870
  lifecycle.getSnapshot
7426
7871
  );
7427
- useImperativeHandle(props.ref, () => Object.freeze({ getSourceState: lifecycle.getSourceState }), [
7428
- lifecycle
7429
- ]);
7430
- useLayoutEffect2(() => {
7872
+ useImperativeHandle(
7873
+ props.ref,
7874
+ () => Object.freeze({ getSourceState: lifecycle.getSourceState, retry: lifecycle.retry }),
7875
+ [lifecycle]
7876
+ );
7877
+ useLayoutEffect3(() => {
7431
7878
  const unmount = lifecycle.mount();
7432
7879
  if (AppState.currentState !== "active") lifecycle.background();
7433
7880
  const subscription = AppState.addEventListener("change", (state) => {
@@ -7439,10 +7886,10 @@ function Map2(props) {
7439
7886
  unmount();
7440
7887
  };
7441
7888
  }, [lifecycle]);
7442
- useLayoutEffect2(() => {
7889
+ useLayoutEffect3(() => {
7443
7890
  lifecycle.update(props);
7444
7891
  });
7445
- return /* @__PURE__ */ jsx2(View2, { style: props.style, testID: props.testID, children: snapshot.renderer ? /* @__PURE__ */ jsx2(
7892
+ return /* @__PURE__ */ jsx3(View2, { style: props.style, testID: props.testID, children: snapshot.renderer ? /* @__PURE__ */ jsx3(
7446
7893
  NativeScene,
7447
7894
  {
7448
7895
  scene: snapshot.renderer,
@@ -7451,6 +7898,7 @@ function Map2(props) {
7451
7898
  interactions: snapshot.interactions,
7452
7899
  interactionEnabled: snapshot.interactionEnabled === true,
7453
7900
  renderMarker: props.renderMarker,
7901
+ boundaryFeatureState: props.boundaryFeatureState,
7454
7902
  children: props.children
7455
7903
  },
7456
7904
  snapshot.renderer.key