@tileflow/react-native 0.1.0-alpha.5 → 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();
@@ -5466,6 +5778,7 @@ function projectMapSourceState(state) {
5466
5778
  // src/theme-motion-input.ts
5467
5779
  import { validateTileflowThemeBlendSelection } from "@tileflow/core/native";
5468
5780
  var maximumTransitionMs = 5e3;
5781
+ var defaultTransitionMs = 450;
5469
5782
  function own(value, keys) {
5470
5783
  const prototype = Object.getPrototypeOf(value);
5471
5784
  if (Array.isArray(value) || prototype !== Object.prototype && prototype !== null)
@@ -5480,9 +5793,9 @@ function own(value, keys) {
5480
5793
  return result;
5481
5794
  }
5482
5795
  function snapshotThemeTransition(value) {
5483
- if (value === void 0) return 0;
5796
+ if (value === void 0) return defaultTransitionMs;
5484
5797
  if (!value || typeof value !== "object") throw new NativePreparationError();
5485
- const { duration = 0 } = own(value, ["duration"]);
5798
+ const { duration = defaultTransitionMs } = own(value, ["duration"]);
5486
5799
  if (typeof duration !== "number" || !Number.isFinite(duration) || duration < 0 || duration > maximumTransitionMs)
5487
5800
  throw new NativePreparationError();
5488
5801
  return Math.round(duration);
@@ -5501,6 +5814,18 @@ function themeBlendKey(blend) {
5501
5814
  }
5502
5815
 
5503
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
+ ]);
5504
5829
  function themeSource(source, name3) {
5505
5830
  const theme = Object.hasOwn(source.map.themes, name3) ? source.map.themes[name3] : void 0;
5506
5831
  if (!theme) throw new NativePreparationError();
@@ -5583,7 +5908,9 @@ function createMountedMapOwner(ports2) {
5583
5908
  let sourceState;
5584
5909
  let current;
5585
5910
  let lastReadiness = "";
5911
+ let reopenings = 0;
5586
5912
  let disposal;
5913
+ const schedule = ports2.schedule ?? timer2;
5587
5914
  const retired = /* @__PURE__ */ new Set();
5588
5915
  const tasks = /* @__PURE__ */ new Set();
5589
5916
  const listeners = /* @__PURE__ */ new Set();
@@ -5618,10 +5945,11 @@ function createMountedMapOwner(ports2) {
5618
5945
  if (lastReadiness === identity) return;
5619
5946
  lastReadiness = identity;
5620
5947
  }
5948
+ if (event.type === "load") reopenings = 0;
5621
5949
  try {
5622
5950
  let result;
5623
5951
  if (event.type === "load") result = props.onLoad?.(event);
5624
- 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")
5625
5953
  result = props.onError?.(event);
5626
5954
  else if (event.type === "readiness-change") result = props.onReadinessChange?.(event);
5627
5955
  else result = props.onThemeChange?.(event);
@@ -5698,11 +6026,29 @@ function createMountedMapOwner(ports2) {
5698
6026
  }
5699
6027
  }
5700
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
+ }
5701
6046
  function retire(epoch) {
5702
6047
  if (epoch.retirement) return epoch.retirement;
5703
6048
  epoch.live = false;
5704
6049
  retired.add(epoch);
5705
6050
  cancelJob(epoch);
6051
+ cancelRetry(epoch);
5706
6052
  epoch.cache.clear();
5707
6053
  epoch.binding.dispose();
5708
6054
  const context2 = epoch.map?.retire();
@@ -5726,6 +6072,24 @@ function createMountedMapOwner(ports2) {
5726
6072
  });
5727
6073
  return track(attempt);
5728
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
+ }
5729
6093
  function createEpoch(developmentOrigin) {
5730
6094
  const epoch = {
5731
6095
  live: true,
@@ -5753,7 +6117,17 @@ function createMountedMapOwner(ports2) {
5753
6117
  if (!epoch.live || current !== epoch) throw new NativePreparationError();
5754
6118
  const binding = await epoch.binding.replace(source);
5755
6119
  if (!epoch.live || current !== epoch) throw new NativePreparationError();
5756
- 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
+ });
5757
6131
  const map = await epoch.lease.ready;
5758
6132
  epoch.map = map;
5759
6133
  if (!epoch.live || current !== epoch || map.state.status !== "active") {
@@ -5762,7 +6136,9 @@ function createMountedMapOwner(ports2) {
5762
6136
  }
5763
6137
  return map;
5764
6138
  });
5765
- void epoch.context.catch(() => void 0);
6139
+ void epoch.context.catch(() => {
6140
+ epoch.contextFailed = true;
6141
+ });
5766
6142
  }
5767
6143
  return epoch.context;
5768
6144
  }
@@ -5904,6 +6280,7 @@ function createMountedMapOwner(ports2) {
5904
6280
  }
5905
6281
  rendererError();
5906
6282
  epoch.renderer?.preparationFailed(source.generation);
6283
+ retryLater(epoch, source);
5907
6284
  }).finally(() => {
5908
6285
  if (epoch.job === job) epoch.job = void 0;
5909
6286
  });
@@ -5961,6 +6338,7 @@ function createMountedMapOwner(ports2) {
5961
6338
  foreground = false;
5962
6339
  if (current) {
5963
6340
  cancelJob(current);
6341
+ cancelRetry(current);
5964
6342
  current.renderer?.background();
5965
6343
  }
5966
6344
  return;
@@ -6059,6 +6437,22 @@ function createMountedMapOwner(ports2) {
6059
6437
  if (state?.status !== "ready") select();
6060
6438
  },
6061
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
+ },
6062
6456
  async whenIdle() {
6063
6457
  while (tasks.size) await Promise.allSettled([...tasks]);
6064
6458
  await current?.renderer?.whenIdle();
@@ -6096,7 +6490,9 @@ var nativeBootstrapLimits = Object.freeze({
6096
6490
  queueDepth: 32,
6097
6491
  requestBytes: 2048,
6098
6492
  responseBytes: 65536,
6099
- timeoutMs: 3e4
6493
+ retryAfterCharacters: 64,
6494
+ // Well inside the 30 s ticket and batch deadlines.
6495
+ timeoutMs: 1e4
6100
6496
  });
6101
6497
 
6102
6498
  // src/native-admission-wire.ts
@@ -6127,10 +6523,11 @@ function decodeBoundedBase64(value) {
6127
6523
  return bytes;
6128
6524
  }
6129
6525
  function responseFromWire(reply, isAborted, release) {
6130
- 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))
6131
6527
  throw invalid4();
6132
6528
  const status = reply.status;
6133
6529
  const cacheControl = reply.cacheControl;
6530
+ const retryAfter = reply.retryAfter || null;
6134
6531
  let bytes = decodeBoundedBase64(reply.bodyBase64);
6135
6532
  let readerTaken = false;
6136
6533
  let done = false;
@@ -6144,7 +6541,9 @@ function responseFromWire(reply, isAborted, release) {
6144
6541
  status,
6145
6542
  headers: Object.freeze({
6146
6543
  get(name3) {
6147
- 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;
6148
6547
  }
6149
6548
  }),
6150
6549
  body: Object.freeze({
@@ -7271,7 +7670,7 @@ function createNativeMapOwner() {
7271
7670
  }
7272
7671
 
7273
7672
  // src/map.tsx
7274
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
7673
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
7275
7674
  var fill = Object.freeze({ flex: 1 });
7276
7675
  function NativeScene({
7277
7676
  scene,
@@ -7280,7 +7679,8 @@ function NativeScene({
7280
7679
  children,
7281
7680
  interactions,
7282
7681
  interactionEnabled,
7283
- renderMarker
7682
+ renderMarker,
7683
+ boundaryFeatureState
7284
7684
  }) {
7285
7685
  const view = useRef(null);
7286
7686
  const camera = useRef(null);
@@ -7290,7 +7690,10 @@ function NativeScene({
7290
7690
  const laidOut = useRef(false);
7291
7691
  const styleLoaded = useRef(false);
7292
7692
  const attachRequested = useRef(false);
7293
- 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(() => {
7294
7697
  const value = Object.freeze({
7295
7698
  key: scene.key,
7296
7699
  style: scene.style,
@@ -7304,7 +7707,7 @@ function NativeScene({
7304
7707
  });
7305
7708
  return value;
7306
7709
  }, [scene.key, scene.style]);
7307
- useLayoutEffect2(() => {
7710
+ useLayoutEffect3(() => {
7308
7711
  committedHost.current = host;
7309
7712
  lifecycle.bindInteractionHost(host);
7310
7713
  return () => {
@@ -7312,11 +7715,11 @@ function NativeScene({
7312
7715
  lifecycle.unbindInteractionHost(host);
7313
7716
  };
7314
7717
  }, [host, lifecycle]);
7315
- const mapStyle = useMemo2(
7718
+ const mapStyle = useMemo3(
7316
7719
  () => JSON.stringify(scene.style),
7317
7720
  [scene.style]
7318
7721
  );
7319
- const [initialViewState] = useState(() => {
7722
+ const [initialViewState] = useState2(() => {
7320
7723
  const center = [scene.initialView.center[0], scene.initialView.center[1]];
7321
7724
  Object.freeze(center);
7322
7725
  return Object.freeze({
@@ -7342,7 +7745,7 @@ function NativeScene({
7342
7745
  };
7343
7746
  const annotations = interactions?.annotations;
7344
7747
  const byId = new globalThis.Map(annotations?.map((annotation) => [annotation.id, annotation]));
7345
- return /* @__PURE__ */ jsx2(
7748
+ return /* @__PURE__ */ jsx3(
7346
7749
  View2,
7347
7750
  {
7348
7751
  ref: bind2,
@@ -7380,10 +7783,11 @@ function NativeScene({
7380
7783
  },
7381
7784
  onDidFinishLoadingStyle: () => {
7382
7785
  styleLoaded.current = true;
7786
+ setLoadedStyle(latestStyle.current);
7383
7787
  attach();
7384
7788
  },
7385
7789
  children: [
7386
- /* @__PURE__ */ jsx2(
7790
+ /* @__PURE__ */ jsx3(
7387
7791
  NativeCamera,
7388
7792
  {
7389
7793
  ref: camera,
@@ -7393,9 +7797,9 @@ function NativeScene({
7393
7797
  },
7394
7798
  "tileflow-camera"
7395
7799
  ),
7396
- /* @__PURE__ */ jsx2(Fragment, { children: interactions?.plan.order.map((id) => {
7800
+ /* @__PURE__ */ jsx3(Fragment, { children: interactions?.plan.order.map((id) => {
7397
7801
  const annotation = byId.get(id);
7398
- return annotation ? /* @__PURE__ */ jsx2(
7802
+ return annotation ? /* @__PURE__ */ jsx3(
7399
7803
  NativeAnnotationMarker,
7400
7804
  {
7401
7805
  annotation,
@@ -7407,6 +7811,15 @@ function NativeScene({
7407
7811
  annotation.id
7408
7812
  ) : null;
7409
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,
7410
7823
  children
7411
7824
  ]
7412
7825
  }
@@ -7415,7 +7828,7 @@ function NativeScene({
7415
7828
  );
7416
7829
  }
7417
7830
  function Map2(props) {
7418
- const [lifecycle] = useState(
7831
+ const [lifecycle] = useState2(
7419
7832
  () => createMapLifecycle(createNativeMapOwner, retireNativeMapOwner)
7420
7833
  );
7421
7834
  const snapshot = useSyncExternalStore(
@@ -7423,10 +7836,12 @@ function Map2(props) {
7423
7836
  lifecycle.getSnapshot,
7424
7837
  lifecycle.getSnapshot
7425
7838
  );
7426
- useImperativeHandle(props.ref, () => Object.freeze({ getSourceState: lifecycle.getSourceState }), [
7427
- lifecycle
7428
- ]);
7429
- useLayoutEffect2(() => {
7839
+ useImperativeHandle(
7840
+ props.ref,
7841
+ () => Object.freeze({ getSourceState: lifecycle.getSourceState, retry: lifecycle.retry }),
7842
+ [lifecycle]
7843
+ );
7844
+ useLayoutEffect3(() => {
7430
7845
  const unmount = lifecycle.mount();
7431
7846
  if (AppState.currentState !== "active") lifecycle.background();
7432
7847
  const subscription = AppState.addEventListener("change", (state) => {
@@ -7438,10 +7853,10 @@ function Map2(props) {
7438
7853
  unmount();
7439
7854
  };
7440
7855
  }, [lifecycle]);
7441
- useLayoutEffect2(() => {
7856
+ useLayoutEffect3(() => {
7442
7857
  lifecycle.update(props);
7443
7858
  });
7444
- 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(
7445
7860
  NativeScene,
7446
7861
  {
7447
7862
  scene: snapshot.renderer,
@@ -7450,6 +7865,7 @@ function Map2(props) {
7450
7865
  interactions: snapshot.interactions,
7451
7866
  interactionEnabled: snapshot.interactionEnabled === true,
7452
7867
  renderMarker: props.renderMarker,
7868
+ boundaryFeatureState: props.boundaryFeatureState,
7453
7869
  children: props.children
7454
7870
  },
7455
7871
  snapshot.renderer.key