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

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,11 @@ 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"]);
1812
1968
  var MOBILE_CREDENTIAL = /^tf_public_[0-9a-f]{48}$/u;
1813
1969
  var MAP_ID = /^map_[A-Za-z0-9_-]{16}$/u;
1814
1970
  var CREDENTIAL_ID = /^key_[A-Za-z0-9_-]{8,80}$/u;
@@ -1847,6 +2003,13 @@ var SUCCESS_KEYS = /* @__PURE__ */ new Set([
1847
2003
  "tilesetIds"
1848
2004
  ]);
1849
2005
  var RESTART_KEYS = /* @__PURE__ */ new Set(["code", "error", "retryWithNewSession", "sessionId"]);
2006
+ var TERMINAL_CODES = /* @__PURE__ */ new Set([
2007
+ "NATIVE_SESSION_INPUT_INVALID",
2008
+ "NATIVE_SESSION_CREDENTIAL_REJECTED",
2009
+ "NATIVE_SESSION_MAP_MISMATCH",
2010
+ "NATIVE_SESSION_MAP_NOT_FOUND",
2011
+ "NATIVE_SESSION_QUOTA_EXCEEDED"
2012
+ ]);
1850
2013
  var ERROR_MESSAGES = {
1851
2014
  NATIVE_SESSION_INPUT_INVALID: "Native session configuration is invalid.",
1852
2015
  NATIVE_SESSION_RESPONSE_TOO_LARGE: "Native session response exceeded its byte limit.",
@@ -1858,6 +2021,7 @@ var ERROR_MESSAGES = {
1858
2021
  NATIVE_SESSION_MAP_NOT_FOUND: "Native session Map was not found.",
1859
2022
  NATIVE_SESSION_QUOTA_EXCEEDED: "Native session quota was exceeded.",
1860
2023
  NATIVE_SESSION_REJECTED: "Native session request was rejected.",
2024
+ NATIVE_SESSION_RATE_LIMITED: "Native session requests are rate limited.",
1861
2025
  NATIVE_SESSION_UNAVAILABLE: "Native session service is unavailable.",
1862
2026
  NATIVE_SESSION_REPLACED: "Native session binding was replaced.",
1863
2027
  NATIVE_SESSION_DISPOSED: "Native session controller is disposed."
@@ -1979,7 +2143,29 @@ function createHostedNativeSessionController(input) {
1979
2143
  })
1980
2144
  );
1981
2145
  }
2146
+ let terminal = null;
2147
+ let failures = 0;
2148
+ let retryAt = null;
2149
+ let retryError = null;
2150
+ function clearRetry() {
2151
+ failures = 0;
2152
+ retryAt = null;
2153
+ retryError = null;
2154
+ }
2155
+ function delayRetry(error) {
2156
+ let at;
2157
+ try {
2158
+ at = readClock();
2159
+ } catch {
2160
+ return;
2161
+ }
2162
+ if (retryAt === null || at >= retryAt) failures = Math.min(failures + 1, 16);
2163
+ const backoff = Math.min(RETRY_INITIAL_MS * 2 ** (failures - 1), RETRY_LIMIT_MS);
2164
+ retryAt = Math.max(retryAt ?? at, at + Math.max(backoff, retryAfterDelays.get(error) ?? 0));
2165
+ retryError = Object.freeze({ code: error.code, kind: error.kind });
2166
+ }
1982
2167
  function rotateIfRequired(at) {
2168
+ if (terminal) throw new HostedNativeSessionError(terminal.code);
1983
2169
  if (!activeSession || binding.kind !== "hosted") return;
1984
2170
  if (at - activeSession.startedAt < ROTATION_AGE_MS && activeSession.requestCount < ROTATION_REQUEST_LIMIT) {
1985
2171
  return;
@@ -2013,6 +2199,7 @@ function createHostedNativeSessionController(input) {
2013
2199
  return current;
2014
2200
  }
2015
2201
  async function ensureAuthority(session, at) {
2202
+ if (terminal) throw new HostedNativeSessionError(terminal.code);
2016
2203
  if (session.authority && session.authority.clockEpoch === clock.epoch && session.authority.validUntil - at > REFRESH_MARGIN_MS) {
2017
2204
  return { authority: session.authority, session };
2018
2205
  }
@@ -2020,20 +2207,28 @@ function createHostedNativeSessionController(input) {
2020
2207
  if (binding.kind !== "hosted") {
2021
2208
  throw new HostedNativeSessionError("NATIVE_SESSION_REPLACED");
2022
2209
  }
2210
+ if (retryAt !== null && retryError && at < retryAt) {
2211
+ if (session.authority && session.authority.clockEpoch === clock.epoch && session.authority.validUntil > at) {
2212
+ return { authority: session.authority, session };
2213
+ }
2214
+ throw new HostedNativeSessionError(retryError.code);
2215
+ }
2023
2216
  const operationBinding = binding;
2024
2217
  const abort = new SessionAbortController();
2025
2218
  operations.add(abort);
2026
2219
  const operation = {
2027
2220
  abort,
2028
2221
  promise: Promise.resolve().then(async () => {
2029
- session.error = null;
2030
- publishSession(session, "loading");
2031
- if (abort.signal.aborted) throw abort.error();
2032
2222
  try {
2223
+ if (abort.signal.aborted) throw abort.error();
2224
+ session.error = null;
2225
+ publishSession(session, "loading");
2226
+ if (abort.signal.aborted) throw abort.error();
2033
2227
  const result = await bootstrap(session, operationBinding, abort, true, true);
2034
2228
  if (abort.signal.aborted) throw abort.error();
2035
2229
  result.session.authority = result.authority;
2036
2230
  result.session.error = null;
2231
+ if (binding === operationBinding) clearRetry();
2037
2232
  if (activeSession === result.session) {
2038
2233
  publishSession(result.session, "ready");
2039
2234
  if (abort.signal.aborted) throw abort.error();
@@ -2042,6 +2237,22 @@ function createHostedNativeSessionController(input) {
2042
2237
  } catch (error) {
2043
2238
  const normalized = abort.signal.aborted ? abort.error() : normalizeError(error);
2044
2239
  const target = latestSession(session);
2240
+ if (normalized.kind === "terminal" && binding === operationBinding && !terminal) {
2241
+ const failure3 = Object.freeze({ code: normalized.code, kind: normalized.kind });
2242
+ terminal = failure3;
2243
+ const rejectedSession = activeSession;
2244
+ if (rejectedSession) {
2245
+ rejectedSession.authority = null;
2246
+ rejectedSession.error = failure3;
2247
+ }
2248
+ for (const pending of [...operations]) pending.abort(failure3.code);
2249
+ if (binding === operationBinding && rejectedSession) {
2250
+ publishSession(rejectedSession, "error", failure3);
2251
+ }
2252
+ }
2253
+ if (normalized.kind === "recoverable" && binding === operationBinding && !terminal) {
2254
+ delayRetry(normalized);
2255
+ }
2045
2256
  if (normalized.code !== "NATIVE_SESSION_REPLACED" && normalized.code !== "NATIVE_SESSION_DISPOSED") {
2046
2257
  target.error = Object.freeze({ code: normalized.code, kind: normalized.kind });
2047
2258
  publishSession(target, "error", target.error);
@@ -2074,7 +2285,10 @@ function createHostedNativeSessionController(input) {
2074
2285
  try {
2075
2286
  body = source.length ? JSON.parse(source) : null;
2076
2287
  } catch {
2077
- throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_JSON_INVALID");
2288
+ if (response.status === 201) {
2289
+ throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_JSON_INVALID");
2290
+ }
2291
+ body = null;
2078
2292
  }
2079
2293
  if (response.status === 409 && allowRestart && isExactRestart(body, session.sessionId)) {
2080
2294
  assertCurrentSession(session, hostedBinding);
@@ -2082,7 +2296,12 @@ function createHostedNativeSessionController(input) {
2082
2296
  if (abort.signal.aborted) throw abort.error();
2083
2297
  return bootstrap(replacement, hostedBinding, abort, false, false);
2084
2298
  }
2085
- if (response.status !== 201) throw statusError(response.status);
2299
+ if (response.status !== 201) {
2300
+ const failure3 = statusError(response.status, body);
2301
+ const delay = retryAfterMs(response.headers, now);
2302
+ if (delay !== null) retryAfterDelays.set(failure3, delay);
2303
+ throw failure3;
2304
+ }
2086
2305
  if (!hasNoStore(response.headers.get("cache-control"))) {
2087
2306
  throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
2088
2307
  }
@@ -2155,6 +2374,16 @@ function createHostedNativeSessionController(input) {
2155
2374
  if (binding.kind === "direct" || !activeSession) {
2156
2375
  return Object.freeze({ status: "idle", source: "direct", lifecycle });
2157
2376
  }
2377
+ if (terminal) {
2378
+ return Object.freeze({
2379
+ status: "error",
2380
+ source: "hosted",
2381
+ lifecycle,
2382
+ mapId: binding.mapId,
2383
+ sessionId: activeSession.sessionId,
2384
+ error: terminal
2385
+ });
2386
+ }
2158
2387
  if (activeSession.operation) {
2159
2388
  return Object.freeze({
2160
2389
  status: "loading",
@@ -2206,6 +2435,7 @@ function createHostedNativeSessionController(input) {
2206
2435
  if (bindingGeneration !== expectedGeneration) {
2207
2436
  throw new HostedNativeSessionError("NATIVE_SESSION_REPLACED");
2208
2437
  }
2438
+ if (terminal) throw new HostedNativeSessionError(terminal.code);
2209
2439
  checkedAt = readClock();
2210
2440
  if (resolved.authority.clockEpoch === clock.epoch && resolved.authority.validUntil > checkedAt) {
2211
2441
  admittedAuthorities.set(resolved.authority.public, resolved.authority);
@@ -2260,7 +2490,7 @@ function createHostedNativeSessionController(input) {
2260
2490
  }
2261
2491
  },
2262
2492
  transportBudget(authority) {
2263
- if (disposed || lifecycle !== "foreground") return 0;
2493
+ if (disposed || terminal || lifecycle !== "foreground") return 0;
2264
2494
  const stored = admittedAuthorities.get(authority);
2265
2495
  if (!stored || stored.bindingGeneration !== bindingGeneration) return 0;
2266
2496
  try {
@@ -2279,6 +2509,11 @@ function createHostedNativeSessionController(input) {
2279
2509
  async resume() {
2280
2510
  if (disposed) throw new HostedNativeSessionError("NATIVE_SESSION_DISPOSED");
2281
2511
  lifecycle = "foreground";
2512
+ if (terminal) {
2513
+ const error = new HostedNativeSessionError(terminal.code);
2514
+ publish(currentSnapshot());
2515
+ throw error;
2516
+ }
2282
2517
  if (binding.kind === "direct" || !activeSession) {
2283
2518
  publish(currentSnapshot());
2284
2519
  return;
@@ -2301,9 +2536,27 @@ function createHostedNativeSessionController(input) {
2301
2536
  bindingGeneration += 1;
2302
2537
  for (const operation of operations) operation.abort("NATIVE_SESSION_REPLACED");
2303
2538
  binding = next;
2539
+ terminal = null;
2540
+ clearRetry();
2304
2541
  activeSession = next.kind === "hosted" ? createSession(next.surfaceId, readClock()) : null;
2305
2542
  publish(snapshotIdle(binding, activeSession, lifecycle));
2306
2543
  },
2544
+ retry() {
2545
+ if (disposed) throw new HostedNativeSessionError("NATIVE_SESSION_DISPOSED");
2546
+ if (!terminal && retryAt === null) return;
2547
+ terminal = null;
2548
+ clearRetry();
2549
+ if (activeSession && !activeSession.operation) activeSession.error = null;
2550
+ publish(currentSnapshot());
2551
+ },
2552
+ retryDelay() {
2553
+ if (disposed || terminal || retryAt === null) return null;
2554
+ try {
2555
+ return Math.max(0, Math.ceil(retryAt - readClock()));
2556
+ } catch {
2557
+ return null;
2558
+ }
2559
+ },
2307
2560
  subscribe(listener) {
2308
2561
  if (typeof listener !== "function") {
2309
2562
  throw new HostedNativeSessionError("NATIVE_SESSION_INPUT_INVALID");
@@ -2398,7 +2651,8 @@ function parseSuccess(value, binding, expectedSessionId, expectedSurfaceId) {
2398
2651
  const issuedAtMs = parseExactIso(value.issuedAt);
2399
2652
  const serverTimeMs = parseExactIso(value.serverTime);
2400
2653
  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) {
2654
+ 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.
2655
+ expiresAtMs - serverTimeMs > MAX_GRANT_LIFETIME_MS) {
2402
2656
  throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
2403
2657
  }
2404
2658
  const authority = {
@@ -2577,18 +2831,46 @@ function decodeUtf8(bytes) {
2577
2831
  function hasNoStore(value) {
2578
2832
  return typeof value === "string" && value.split(",").some((part) => part.trim().toLowerCase() === "no-store");
2579
2833
  }
2580
- function statusError(status) {
2834
+ function statusError(status, body) {
2581
2835
  if (status === 401) return new HostedNativeSessionError("NATIVE_SESSION_CREDENTIAL_REJECTED");
2582
2836
  if (status === 403) return new HostedNativeSessionError("NATIVE_SESSION_MAP_MISMATCH");
2583
2837
  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");
2838
+ if (status === 429) {
2839
+ return new HostedNativeSessionError(
2840
+ isRecord(body) && typeof body.code === "string" && QUOTA_CODES.has(body.code) ? "NATIVE_SESSION_QUOTA_EXCEEDED" : "NATIVE_SESSION_RATE_LIMITED"
2841
+ );
2842
+ }
2843
+ if (status === 408 || status >= 500 && status <= 599) {
2844
+ return new HostedNativeSessionError("NATIVE_SESSION_UNAVAILABLE");
2845
+ }
2586
2846
  return new HostedNativeSessionError("NATIVE_SESSION_REJECTED");
2587
2847
  }
2848
+ var retryAfterDelays = /* @__PURE__ */ new WeakMap();
2849
+ function retryAfterMs(headers, now) {
2850
+ let value;
2851
+ try {
2852
+ value = headers.get("retry-after");
2853
+ } catch {
2854
+ return null;
2855
+ }
2856
+ if (typeof value !== "string" || value.length > 64) return null;
2857
+ const text = value.trim();
2858
+ let delay;
2859
+ if (RETRY_AFTER_SECONDS.test(text)) delay = Number(text) * 1e3;
2860
+ else {
2861
+ const date = Date.parse(text);
2862
+ if (!Number.isFinite(date)) return null;
2863
+ try {
2864
+ delay = date - readWallClock(now);
2865
+ } catch {
2866
+ return null;
2867
+ }
2868
+ }
2869
+ return Math.min(Math.max(0, delay), RETRY_AFTER_LIMIT_MS);
2870
+ }
2588
2871
  function errorKind(code) {
2589
2872
  if (code === "NATIVE_SESSION_DISPOSED" || code === "NATIVE_SESSION_REPLACED") return "cancelled";
2590
- if (code === "NATIVE_SESSION_UNAVAILABLE") return "recoverable";
2591
- return "terminal";
2873
+ return TERMINAL_CODES.has(code) ? "terminal" : "recoverable";
2592
2874
  }
2593
2875
  function normalizeError(error, fallback = "NATIVE_SESSION_RESPONSE_INVALID") {
2594
2876
  return error instanceof HostedNativeSessionError ? error : new HostedNativeSessionError(fallback);
@@ -2887,6 +3169,8 @@ function createNativeAdmissionOwner(options) {
2887
3169
  try {
2888
3170
  resources = normalizeNativeResources(input.resources);
2889
3171
  if (input.onRetired !== void 0 && typeof input.onRetired !== "function") throw cancel();
3172
+ if (input.onSessionError !== void 0 && typeof input.onSessionError !== "function")
3173
+ throw cancel();
2890
3174
  } catch {
2891
3175
  throw cancel("NATIVE_ADMISSION_INVALID");
2892
3176
  }
@@ -2947,6 +3231,17 @@ function createNativeAdmissionOwner(options) {
2947
3231
  controller,
2948
3232
  handle: void 0
2949
3233
  };
3234
+ let reported = null;
3235
+ const releaseSession = controller.subscribe((state) => {
3236
+ const code = state.status === "error" ? state.error.code : null;
3237
+ if (!context.live || code === reported) return;
3238
+ reported = code;
3239
+ if (state.status !== "error") return;
3240
+ try {
3241
+ input.onSessionError?.(Object.freeze({ code: state.error.code, kind: state.error.kind }));
3242
+ } catch {
3243
+ }
3244
+ });
2950
3245
  context.handle = Object.freeze({
2951
3246
  context: context.id,
2952
3247
  generation: context.generation,
@@ -2976,9 +3271,26 @@ function createNativeAdmissionOwner(options) {
2976
3271
  discriminate: catalog.discriminate,
2977
3272
  discriminateForTest: catalog.discriminate,
2978
3273
  extendResources: catalog.extend,
3274
+ retry() {
3275
+ if (!context.live || disposed) return;
3276
+ try {
3277
+ controller.retry?.();
3278
+ } catch {
3279
+ }
3280
+ },
3281
+ retryDelay() {
3282
+ if (!context.live || disposed) return null;
3283
+ try {
3284
+ const delay = controller.retryDelay?.() ?? null;
3285
+ return delay !== null && Number.isSafeInteger(delay) && delay >= 0 ? delay : null;
3286
+ } catch {
3287
+ return null;
3288
+ }
3289
+ },
2979
3290
  retire() {
2980
3291
  if (retirement) return retirement;
2981
3292
  context.live = false;
3293
+ releaseSession();
2982
3294
  context.batch = null;
2983
3295
  context.tickets.clear();
2984
3296
  catalog.retire();
@@ -5502,6 +5814,18 @@ function themeBlendKey(blend) {
5502
5814
  }
5503
5815
 
5504
5816
  // src/mounted-map-owner.ts
5817
+ var timer2 = (run, delayMs) => {
5818
+ const handle = setTimeout(run, delayMs);
5819
+ return () => clearTimeout(handle);
5820
+ };
5821
+ var REOPEN_LIMIT = 3;
5822
+ var sessionErrorCodes = /* @__PURE__ */ new Set([
5823
+ "NATIVE_SESSION_CREDENTIAL_REJECTED",
5824
+ "NATIVE_SESSION_MAP_MISMATCH",
5825
+ "NATIVE_SESSION_MAP_NOT_FOUND",
5826
+ "NATIVE_SESSION_QUOTA_EXCEEDED",
5827
+ "NATIVE_SESSION_INPUT_INVALID"
5828
+ ]);
5505
5829
  function themeSource(source, name3) {
5506
5830
  const theme = Object.hasOwn(source.map.themes, name3) ? source.map.themes[name3] : void 0;
5507
5831
  if (!theme) throw new NativePreparationError();
@@ -5584,7 +5908,9 @@ function createMountedMapOwner(ports2) {
5584
5908
  let sourceState;
5585
5909
  let current;
5586
5910
  let lastReadiness = "";
5911
+ let reopenings = 0;
5587
5912
  let disposal;
5913
+ const schedule = ports2.schedule ?? timer2;
5588
5914
  const retired = /* @__PURE__ */ new Set();
5589
5915
  const tasks = /* @__PURE__ */ new Set();
5590
5916
  const listeners = /* @__PURE__ */ new Set();
@@ -5619,10 +5945,11 @@ function createMountedMapOwner(ports2) {
5619
5945
  if (lastReadiness === identity) return;
5620
5946
  lastReadiness = identity;
5621
5947
  }
5948
+ if (event.type === "load") reopenings = 0;
5622
5949
  try {
5623
5950
  let result;
5624
5951
  if (event.type === "load") result = props.onLoad?.(event);
5625
- else if (event.type === "renderer-error" || event.type === "source-error")
5952
+ else if (event.type === "renderer-error" || event.type === "source-error" || event.type === "session-error")
5626
5953
  result = props.onError?.(event);
5627
5954
  else if (event.type === "readiness-change") result = props.onReadinessChange?.(event);
5628
5955
  else result = props.onThemeChange?.(event);
@@ -5699,11 +6026,29 @@ function createMountedMapOwner(ports2) {
5699
6026
  }
5700
6027
  }
5701
6028
  }
6029
+ function cancelRetry(epoch) {
6030
+ const cancel = epoch.retry;
6031
+ epoch.retry = void 0;
6032
+ cancel?.();
6033
+ }
6034
+ function retryLater(epoch, source) {
6035
+ const delay = epoch.map?.retryDelay();
6036
+ if (typeof delay !== "number") return;
6037
+ cancelRetry(epoch);
6038
+ const cancel = schedule(() => {
6039
+ if (epoch.retry !== cancel) return;
6040
+ epoch.retry = void 0;
6041
+ if (!disposed && foreground && current === epoch && epoch.live && core.state === source)
6042
+ prepare(epoch, source);
6043
+ }, delay);
6044
+ epoch.retry = cancel;
6045
+ }
5702
6046
  function retire(epoch) {
5703
6047
  if (epoch.retirement) return epoch.retirement;
5704
6048
  epoch.live = false;
5705
6049
  retired.add(epoch);
5706
6050
  cancelJob(epoch);
6051
+ cancelRetry(epoch);
5707
6052
  epoch.cache.clear();
5708
6053
  epoch.binding.dispose();
5709
6054
  const context2 = epoch.map?.retire();
@@ -5727,6 +6072,24 @@ function createMountedMapOwner(ports2) {
5727
6072
  });
5728
6073
  return track(attempt);
5729
6074
  }
6075
+ function restart(epoch) {
6076
+ current = void 0;
6077
+ void retire(epoch).catch(() => void 0);
6078
+ current = createEpoch(epoch.developmentOrigin);
6079
+ select();
6080
+ }
6081
+ function reopen(epoch) {
6082
+ if (disposed || current !== epoch || !epoch.live || !props) return;
6083
+ rendererError();
6084
+ if (reopenings >= REOPEN_LIMIT) return;
6085
+ reopenings += 1;
6086
+ restart(epoch);
6087
+ }
6088
+ function sessionError(epoch, code) {
6089
+ if (disposed || current !== epoch || !epoch.live || !sessionErrorCodes.has(code)) return;
6090
+ const error = Object.freeze({ code, kind: "terminal" });
6091
+ emit({ type: "session-error", generation: generation(), error });
6092
+ }
5730
6093
  function createEpoch(developmentOrigin) {
5731
6094
  const epoch = {
5732
6095
  live: true,
@@ -5754,7 +6117,17 @@ function createMountedMapOwner(ports2) {
5754
6117
  if (!epoch.live || current !== epoch) throw new NativePreparationError();
5755
6118
  const binding = await epoch.binding.replace(source);
5756
6119
  if (!epoch.live || current !== epoch) throw new NativePreparationError();
5757
- epoch.lease = ports2.installation.open({ binding, resources: [], now: ports2.now });
6120
+ epoch.lease = ports2.installation.open({
6121
+ binding,
6122
+ resources: [],
6123
+ now: ports2.now,
6124
+ // Admission reports from inside its own cleanup and session publication; defer.
6125
+ onRetired: () => void Promise.resolve().then(() => reopen(epoch)),
6126
+ onSessionError: (error) => {
6127
+ if (error.kind !== "terminal") return;
6128
+ void Promise.resolve().then(() => sessionError(epoch, error.code));
6129
+ }
6130
+ });
5758
6131
  const map = await epoch.lease.ready;
5759
6132
  epoch.map = map;
5760
6133
  if (!epoch.live || current !== epoch || map.state.status !== "active") {
@@ -5763,7 +6136,9 @@ function createMountedMapOwner(ports2) {
5763
6136
  }
5764
6137
  return map;
5765
6138
  });
5766
- void epoch.context.catch(() => void 0);
6139
+ void epoch.context.catch(() => {
6140
+ epoch.contextFailed = true;
6141
+ });
5767
6142
  }
5768
6143
  return epoch.context;
5769
6144
  }
@@ -5905,6 +6280,7 @@ function createMountedMapOwner(ports2) {
5905
6280
  }
5906
6281
  rendererError();
5907
6282
  epoch.renderer?.preparationFailed(source.generation);
6283
+ retryLater(epoch, source);
5908
6284
  }).finally(() => {
5909
6285
  if (epoch.job === job) epoch.job = void 0;
5910
6286
  });
@@ -5962,6 +6338,7 @@ function createMountedMapOwner(ports2) {
5962
6338
  foreground = false;
5963
6339
  if (current) {
5964
6340
  cancelJob(current);
6341
+ cancelRetry(current);
5965
6342
  current.renderer?.background();
5966
6343
  }
5967
6344
  return;
@@ -6060,6 +6437,22 @@ function createMountedMapOwner(ports2) {
6060
6437
  if (state?.status !== "ready") select();
6061
6438
  },
6062
6439
  nativeLifecycle,
6440
+ /** Loads again after a stopped session, a failed source or a failed context. */
6441
+ retry() {
6442
+ const epoch = current;
6443
+ if (disposed || !props || !epoch?.live) return;
6444
+ reopenings = 0;
6445
+ const map = epoch.map;
6446
+ if (epoch.contextFailed || map?.state.status === "retired") {
6447
+ restart(epoch);
6448
+ return;
6449
+ }
6450
+ map?.retry();
6451
+ cancelRetry(epoch);
6452
+ const state = core.state;
6453
+ if (state?.status === "error") select();
6454
+ else if (state?.status === "ready" && map && foreground) prepare(epoch, state);
6455
+ },
6063
6456
  async whenIdle() {
6064
6457
  while (tasks.size) await Promise.allSettled([...tasks]);
6065
6458
  await current?.renderer?.whenIdle();
@@ -6097,7 +6490,9 @@ var nativeBootstrapLimits = Object.freeze({
6097
6490
  queueDepth: 32,
6098
6491
  requestBytes: 2048,
6099
6492
  responseBytes: 65536,
6100
- timeoutMs: 3e4
6493
+ retryAfterCharacters: 64,
6494
+ // Well inside the 30 s ticket and batch deadlines.
6495
+ timeoutMs: 1e4
6101
6496
  });
6102
6497
 
6103
6498
  // src/native-admission-wire.ts
@@ -6128,10 +6523,11 @@ function decodeBoundedBase64(value) {
6128
6523
  return bytes;
6129
6524
  }
6130
6525
  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)
6526
+ 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
6527
  throw invalid4();
6133
6528
  const status = reply.status;
6134
6529
  const cacheControl = reply.cacheControl;
6530
+ const retryAfter = reply.retryAfter || null;
6135
6531
  let bytes = decodeBoundedBase64(reply.bodyBase64);
6136
6532
  let readerTaken = false;
6137
6533
  let done = false;
@@ -6145,7 +6541,9 @@ function responseFromWire(reply, isAborted, release) {
6145
6541
  status,
6146
6542
  headers: Object.freeze({
6147
6543
  get(name3) {
6148
- return name3.toLowerCase() === "cache-control" ? cacheControl : null;
6544
+ const header = name3.toLowerCase();
6545
+ if (header === "cache-control") return cacheControl;
6546
+ return header === "retry-after" ? retryAfter : null;
6149
6547
  }
6150
6548
  }),
6151
6549
  body: Object.freeze({
@@ -7272,7 +7670,7 @@ function createNativeMapOwner() {
7272
7670
  }
7273
7671
 
7274
7672
  // src/map.tsx
7275
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
7673
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
7276
7674
  var fill = Object.freeze({ flex: 1 });
7277
7675
  function NativeScene({
7278
7676
  scene,
@@ -7281,7 +7679,8 @@ function NativeScene({
7281
7679
  children,
7282
7680
  interactions,
7283
7681
  interactionEnabled,
7284
- renderMarker
7682
+ renderMarker,
7683
+ boundaryFeatureState
7285
7684
  }) {
7286
7685
  const view = useRef(null);
7287
7686
  const camera = useRef(null);
@@ -7291,7 +7690,10 @@ function NativeScene({
7291
7690
  const laidOut = useRef(false);
7292
7691
  const styleLoaded = useRef(false);
7293
7692
  const attachRequested = useRef(false);
7294
- const host = useMemo2(() => {
7693
+ const latestStyle = useRef(scene.style);
7694
+ latestStyle.current = scene.style;
7695
+ const [loadedStyle, setLoadedStyle] = useState2(void 0);
7696
+ const host = useMemo3(() => {
7295
7697
  const value = Object.freeze({
7296
7698
  key: scene.key,
7297
7699
  style: scene.style,
@@ -7305,7 +7707,7 @@ function NativeScene({
7305
7707
  });
7306
7708
  return value;
7307
7709
  }, [scene.key, scene.style]);
7308
- useLayoutEffect2(() => {
7710
+ useLayoutEffect3(() => {
7309
7711
  committedHost.current = host;
7310
7712
  lifecycle.bindInteractionHost(host);
7311
7713
  return () => {
@@ -7313,11 +7715,11 @@ function NativeScene({
7313
7715
  lifecycle.unbindInteractionHost(host);
7314
7716
  };
7315
7717
  }, [host, lifecycle]);
7316
- const mapStyle = useMemo2(
7718
+ const mapStyle = useMemo3(
7317
7719
  () => JSON.stringify(scene.style),
7318
7720
  [scene.style]
7319
7721
  );
7320
- const [initialViewState] = useState(() => {
7722
+ const [initialViewState] = useState2(() => {
7321
7723
  const center = [scene.initialView.center[0], scene.initialView.center[1]];
7322
7724
  Object.freeze(center);
7323
7725
  return Object.freeze({
@@ -7343,7 +7745,7 @@ function NativeScene({
7343
7745
  };
7344
7746
  const annotations = interactions?.annotations;
7345
7747
  const byId = new globalThis.Map(annotations?.map((annotation) => [annotation.id, annotation]));
7346
- return /* @__PURE__ */ jsx2(
7748
+ return /* @__PURE__ */ jsx3(
7347
7749
  View2,
7348
7750
  {
7349
7751
  ref: bind2,
@@ -7381,10 +7783,11 @@ function NativeScene({
7381
7783
  },
7382
7784
  onDidFinishLoadingStyle: () => {
7383
7785
  styleLoaded.current = true;
7786
+ setLoadedStyle(latestStyle.current);
7384
7787
  attach();
7385
7788
  },
7386
7789
  children: [
7387
- /* @__PURE__ */ jsx2(
7790
+ /* @__PURE__ */ jsx3(
7388
7791
  NativeCamera,
7389
7792
  {
7390
7793
  ref: camera,
@@ -7394,9 +7797,9 @@ function NativeScene({
7394
7797
  },
7395
7798
  "tileflow-camera"
7396
7799
  ),
7397
- /* @__PURE__ */ jsx2(Fragment, { children: interactions?.plan.order.map((id) => {
7800
+ /* @__PURE__ */ jsx3(Fragment, { children: interactions?.plan.order.map((id) => {
7398
7801
  const annotation = byId.get(id);
7399
- return annotation ? /* @__PURE__ */ jsx2(
7802
+ return annotation ? /* @__PURE__ */ jsx3(
7400
7803
  NativeAnnotationMarker,
7401
7804
  {
7402
7805
  annotation,
@@ -7408,6 +7811,15 @@ function NativeScene({
7408
7811
  annotation.id
7409
7812
  ) : null;
7410
7813
  }) }, "tileflow-annotations"),
7814
+ boundaryFeatureState ? /* @__PURE__ */ jsx3(
7815
+ NativeBoundaryFeatureState,
7816
+ {
7817
+ input: boundaryFeatureState,
7818
+ style: scene.style,
7819
+ loaded: loadedStyle === scene.style
7820
+ },
7821
+ "tileflow-boundary-feature-state"
7822
+ ) : null,
7411
7823
  children
7412
7824
  ]
7413
7825
  }
@@ -7416,7 +7828,7 @@ function NativeScene({
7416
7828
  );
7417
7829
  }
7418
7830
  function Map2(props) {
7419
- const [lifecycle] = useState(
7831
+ const [lifecycle] = useState2(
7420
7832
  () => createMapLifecycle(createNativeMapOwner, retireNativeMapOwner)
7421
7833
  );
7422
7834
  const snapshot = useSyncExternalStore(
@@ -7424,10 +7836,12 @@ function Map2(props) {
7424
7836
  lifecycle.getSnapshot,
7425
7837
  lifecycle.getSnapshot
7426
7838
  );
7427
- useImperativeHandle(props.ref, () => Object.freeze({ getSourceState: lifecycle.getSourceState }), [
7428
- lifecycle
7429
- ]);
7430
- useLayoutEffect2(() => {
7839
+ useImperativeHandle(
7840
+ props.ref,
7841
+ () => Object.freeze({ getSourceState: lifecycle.getSourceState, retry: lifecycle.retry }),
7842
+ [lifecycle]
7843
+ );
7844
+ useLayoutEffect3(() => {
7431
7845
  const unmount = lifecycle.mount();
7432
7846
  if (AppState.currentState !== "active") lifecycle.background();
7433
7847
  const subscription = AppState.addEventListener("change", (state) => {
@@ -7439,10 +7853,10 @@ function Map2(props) {
7439
7853
  unmount();
7440
7854
  };
7441
7855
  }, [lifecycle]);
7442
- useLayoutEffect2(() => {
7856
+ useLayoutEffect3(() => {
7443
7857
  lifecycle.update(props);
7444
7858
  });
7445
- return /* @__PURE__ */ jsx2(View2, { style: props.style, testID: props.testID, children: snapshot.renderer ? /* @__PURE__ */ jsx2(
7859
+ return /* @__PURE__ */ jsx3(View2, { style: props.style, testID: props.testID, children: snapshot.renderer ? /* @__PURE__ */ jsx3(
7446
7860
  NativeScene,
7447
7861
  {
7448
7862
  scene: snapshot.renderer,
@@ -7451,6 +7865,7 @@ function Map2(props) {
7451
7865
  interactions: snapshot.interactions,
7452
7866
  interactionEnabled: snapshot.interactionEnabled === true,
7453
7867
  renderMarker: props.renderMarker,
7868
+ boundaryFeatureState: props.boundaryFeatureState,
7454
7869
  children: props.children
7455
7870
  },
7456
7871
  snapshot.renderer.key