@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.
@@ -1,4 +1,4 @@
1
- import { M as MapView, a as MapViewChangeEvent, b as MapInitialViewInputs, c as MapCameraProps } from '../contract-By3URNxS.js';
1
+ import { M as MapView, a as MapViewChangeEvent, b as MapInitialViewInputs, c as MapCameraProps } from '../contract-BWT1jxqg.js';
2
2
  import '@maplibre/maplibre-react-native';
3
3
  import 'react';
4
4
  import 'react-native';
@@ -1,4 +1,4 @@
1
- import { b as MapInitialViewInputs, M as MapView } from '../contract-By3URNxS.js';
1
+ import { b as MapInitialViewInputs, M as MapView } from '../contract-BWT1jxqg.js';
2
2
  import '@maplibre/maplibre-react-native';
3
3
  import 'react';
4
4
  import 'react-native';
@@ -44,7 +44,9 @@ var nativeBootstrapLimits = Object.freeze({
44
44
  queueDepth: 32,
45
45
  requestBytes: 2048,
46
46
  responseBytes: 65536,
47
- timeoutMs: 3e4
47
+ retryAfterCharacters: 64,
48
+ // Well inside the 30 s ticket and batch deadlines.
49
+ timeoutMs: 1e4
48
50
  });
49
51
 
50
52
  // src/native-admission-wire.ts
@@ -75,10 +77,11 @@ function decodeBoundedBase64(value) {
75
77
  return bytes;
76
78
  }
77
79
  function responseFromWire(reply, isAborted, release) {
78
- if (!reply || !Number.isInteger(reply.status) || reply.status < 100 || reply.status > 599 || typeof reply.cacheControl !== "string" || reply.cacheControl.length > 1024)
80
+ 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))
79
81
  throw invalid();
80
82
  const status = reply.status;
81
83
  const cacheControl = reply.cacheControl;
84
+ const retryAfter = reply.retryAfter || null;
82
85
  let bytes = decodeBoundedBase64(reply.bodyBase64);
83
86
  let readerTaken = false;
84
87
  let done = false;
@@ -92,7 +95,9 @@ function responseFromWire(reply, isAborted, release) {
92
95
  status,
93
96
  headers: Object.freeze({
94
97
  get(name) {
95
- return name.toLowerCase() === "cache-control" ? cacheControl : null;
98
+ const header = name.toLowerCase();
99
+ if (header === "cache-control") return cacheControl;
100
+ return header === "retry-after" ? retryAfter : null;
96
101
  }
97
102
  }),
98
103
  body: Object.freeze({
@@ -1,5 +1,5 @@
1
1
  import { e as NativeAdmissionCode, a as NativeAdmissionResource, f as NativeAdmissionBridge, d as NativeRemovalAck } from '../native-admission-contract-DFLfUP9X.js';
2
- import { HostedNativeResourcePolicy, createHostedNativeSessionController, HostedNativeSessionFetch, HostedNativeSessionController } from './session.js';
2
+ import { HostedNativeResourcePolicy, createHostedNativeSessionController, HostedNativeSessionFetch, HostedNativeSessionErrorCode, HostedNativeSessionErrorKind, HostedNativeSessionController } from './session.js';
3
3
 
4
4
  type SessionInput = Parameters<typeof createHostedNativeSessionController>[0];
5
5
  type NativeMapAdmissionInput = Omit<SessionInput, 'fetch' | 'sessionIdFactory'> & Readonly<{
@@ -7,6 +7,12 @@ type NativeMapAdmissionInput = Omit<SessionInput, 'fetch' | 'sessionIdFactory'>
7
7
  sessionIdFactory?: () => string;
8
8
  resources: readonly NativeAdmissionResource[];
9
9
  onRetired?: () => void;
10
+ /** Each time the session enters an error, including after first preparation. */
11
+ onSessionError?: (error: Readonly<{
12
+ code: HostedNativeSessionErrorCode;
13
+ kind: HostedNativeSessionErrorKind;
14
+ reason?: string;
15
+ }>) => void;
10
16
  }>;
11
17
  type NativeMapAdmission = Readonly<{
12
18
  context: string;
@@ -25,6 +31,10 @@ type NativeMapAdmission = Readonly<{
25
31
  extendResources(resources: readonly NativeAdmissionResource[]): Promise<Readonly<{
26
32
  resources: number;
27
33
  }>>;
34
+ /** Lets a stopped or delayed session bootstrap again on its next acquisition. */
35
+ retry(): void;
36
+ /** Milliseconds until the session may bootstrap after a recoverable failure, or null. */
37
+ retryDelay(): number | null;
28
38
  retire(): Promise<Readonly<{
29
39
  retired: true;
30
40
  }>>;
@@ -63,6 +73,10 @@ declare function createNativeAdmissionOwner(options: {
63
73
  extendResources(resources: readonly NativeAdmissionResource[]): Promise<Readonly<{
64
74
  resources: number;
65
75
  }>>;
76
+ /** Lets a stopped or delayed session bootstrap again on its next acquisition. */
77
+ retry(): void;
78
+ /** Milliseconds until the session may bootstrap after a recoverable failure, or null. */
79
+ retryDelay(): number | null;
66
80
  retire(): Promise<Readonly<{
67
81
  retired: true;
68
82
  }>>;
@@ -338,6 +338,13 @@ var REFRESH_MARGIN_MS = 3e4;
338
338
  var ROTATION_AGE_MS = 6 * 60 * 60 * 1e3;
339
339
  var ROTATION_REQUEST_LIMIT = 1e4;
340
340
  var FUTURE_ISSUE_SKEW_MS = 3e4;
341
+ var RETRY_INITIAL_MS = 1e3;
342
+ var RETRY_LIMIT_MS = 6e4;
343
+ var RETRY_AFTER_LIMIT_MS = 15 * 60 * 1e3;
344
+ var RETRY_AFTER_SECONDS = /^\d{1,10}$/u;
345
+ var QUOTA_CODES = /* @__PURE__ */ new Set(["COMMERCIAL_MAP_LIMIT_REACHED", "COMMERCIAL_SPENDING_LIMIT_REACHED"]);
346
+ var SERVICE_CODE = /^[A-Z][A-Z0-9_]{0,63}$/u;
347
+ var SERVICE_CODE_BODY_LIMIT = 4096;
341
348
  var MOBILE_CREDENTIAL = /^tf_public_[0-9a-f]{48}$/u;
342
349
  var MAP_ID = /^map_[A-Za-z0-9_-]{16}$/u;
343
350
  var CREDENTIAL_ID = /^key_[A-Za-z0-9_-]{8,80}$/u;
@@ -376,6 +383,14 @@ var SUCCESS_KEYS = /* @__PURE__ */ new Set([
376
383
  "tilesetIds"
377
384
  ]);
378
385
  var RESTART_KEYS = /* @__PURE__ */ new Set(["code", "error", "retryWithNewSession", "sessionId"]);
386
+ var TERMINAL_CODES = /* @__PURE__ */ new Set([
387
+ "NATIVE_SESSION_INPUT_INVALID",
388
+ "NATIVE_SESSION_CREDENTIAL_REJECTED",
389
+ "NATIVE_SESSION_MAP_MISMATCH",
390
+ "NATIVE_SESSION_MAP_NOT_FOUND",
391
+ "NATIVE_SESSION_QUOTA_EXCEEDED",
392
+ "NATIVE_SESSION_REJECTED"
393
+ ]);
379
394
  var ERROR_MESSAGES = {
380
395
  NATIVE_SESSION_INPUT_INVALID: "Native session configuration is invalid.",
381
396
  NATIVE_SESSION_RESPONSE_TOO_LARGE: "Native session response exceeded its byte limit.",
@@ -387,18 +402,22 @@ var ERROR_MESSAGES = {
387
402
  NATIVE_SESSION_MAP_NOT_FOUND: "Native session Map was not found.",
388
403
  NATIVE_SESSION_QUOTA_EXCEEDED: "Native session quota was exceeded.",
389
404
  NATIVE_SESSION_REJECTED: "Native session request was rejected.",
405
+ NATIVE_SESSION_RATE_LIMITED: "Native session requests are rate limited.",
390
406
  NATIVE_SESSION_UNAVAILABLE: "Native session service is unavailable.",
391
407
  NATIVE_SESSION_REPLACED: "Native session binding was replaced.",
392
408
  NATIVE_SESSION_DISPOSED: "Native session controller is disposed."
393
409
  };
394
410
  var HostedNativeSessionError = class extends Error {
395
- constructor(code) {
411
+ constructor(code, reason) {
396
412
  super(ERROR_MESSAGES[code]);
397
413
  __publicField(this, "code");
398
414
  __publicField(this, "kind");
399
415
  this.name = "HostedNativeSessionError";
400
416
  this.code = code;
401
417
  this.kind = errorKind(code);
418
+ if (code === "NATIVE_SESSION_REJECTED" && typeof reason === "string" && SERVICE_CODE.test(reason)) {
419
+ this.reason = reason;
420
+ }
402
421
  }
403
422
  };
404
423
  function createHostedNativeSessionController(input) {
@@ -508,7 +527,29 @@ function createHostedNativeSessionController(input) {
508
527
  })
509
528
  );
510
529
  }
530
+ let terminal = null;
531
+ let failures = 0;
532
+ let retryAt = null;
533
+ let retryError = null;
534
+ function clearRetry() {
535
+ failures = 0;
536
+ retryAt = null;
537
+ retryError = null;
538
+ }
539
+ function delayRetry(error) {
540
+ let at;
541
+ try {
542
+ at = readClock();
543
+ } catch {
544
+ return;
545
+ }
546
+ if (retryAt === null || at >= retryAt) failures = Math.min(failures + 1, 16);
547
+ const backoff = Math.min(RETRY_INITIAL_MS * 2 ** (failures - 1), RETRY_LIMIT_MS);
548
+ retryAt = Math.max(retryAt ?? at, at + Math.max(backoff, retryAfterDelays.get(error) ?? 0));
549
+ retryError = safeError(error);
550
+ }
511
551
  function rotateIfRequired(at) {
552
+ if (terminal) throw errorFrom(terminal);
512
553
  if (!activeSession || binding.kind !== "hosted") return;
513
554
  if (at - activeSession.startedAt < ROTATION_AGE_MS && activeSession.requestCount < ROTATION_REQUEST_LIMIT) {
514
555
  return;
@@ -542,6 +583,7 @@ function createHostedNativeSessionController(input) {
542
583
  return current;
543
584
  }
544
585
  async function ensureAuthority(session, at) {
586
+ if (terminal) throw errorFrom(terminal);
545
587
  if (session.authority && session.authority.clockEpoch === clock.epoch && session.authority.validUntil - at > REFRESH_MARGIN_MS) {
546
588
  return { authority: session.authority, session };
547
589
  }
@@ -549,20 +591,28 @@ function createHostedNativeSessionController(input) {
549
591
  if (binding.kind !== "hosted") {
550
592
  throw new HostedNativeSessionError("NATIVE_SESSION_REPLACED");
551
593
  }
594
+ if (retryAt !== null && retryError && at < retryAt) {
595
+ if (session.authority && session.authority.clockEpoch === clock.epoch && session.authority.validUntil > at) {
596
+ return { authority: session.authority, session };
597
+ }
598
+ throw errorFrom(retryError);
599
+ }
552
600
  const operationBinding = binding;
553
601
  const abort = new SessionAbortController();
554
602
  operations.add(abort);
555
603
  const operation = {
556
604
  abort,
557
605
  promise: Promise.resolve().then(async () => {
558
- session.error = null;
559
- publishSession(session, "loading");
560
- if (abort.signal.aborted) throw abort.error();
561
606
  try {
607
+ if (abort.signal.aborted) throw abort.error();
608
+ session.error = null;
609
+ publishSession(session, "loading");
610
+ if (abort.signal.aborted) throw abort.error();
562
611
  const result = await bootstrap(session, operationBinding, abort, true, true);
563
612
  if (abort.signal.aborted) throw abort.error();
564
613
  result.session.authority = result.authority;
565
614
  result.session.error = null;
615
+ if (binding === operationBinding) clearRetry();
566
616
  if (activeSession === result.session) {
567
617
  publishSession(result.session, "ready");
568
618
  if (abort.signal.aborted) throw abort.error();
@@ -571,8 +621,26 @@ function createHostedNativeSessionController(input) {
571
621
  } catch (error) {
572
622
  const normalized = abort.signal.aborted ? abort.error() : normalizeError(error);
573
623
  const target = latestSession(session);
624
+ if (normalized.kind === "terminal" && binding === operationBinding && !terminal) {
625
+ const failure = safeError(normalized);
626
+ terminal = failure;
627
+ const rejectedSession = activeSession;
628
+ if (rejectedSession) {
629
+ rejectedSession.authority = null;
630
+ rejectedSession.error = failure;
631
+ }
632
+ for (const pending of [...operations]) {
633
+ pending.abort(failure.code, failure.reason);
634
+ }
635
+ if (binding === operationBinding && rejectedSession) {
636
+ publishSession(rejectedSession, "error", failure);
637
+ }
638
+ }
639
+ if (normalized.kind === "recoverable" && binding === operationBinding && !terminal) {
640
+ delayRetry(normalized);
641
+ }
574
642
  if (normalized.code !== "NATIVE_SESSION_REPLACED" && normalized.code !== "NATIVE_SESSION_DISPOSED") {
575
- target.error = Object.freeze({ code: normalized.code, kind: normalized.kind });
643
+ target.error = safeError(normalized);
576
644
  publishSession(target, "error", target.error);
577
645
  }
578
646
  throw normalized;
@@ -598,20 +666,29 @@ function createHostedNativeSessionController(input) {
598
666
  if (abort.signal.aborted) throw abort.error();
599
667
  const expectedSurfaceId = session.authority?.public.surfaceId ?? session.requestedSurfaceId;
600
668
  const response = await fetchResponse(hostedBinding, session, expectedSurfaceId, abort);
601
- const source = await readBoundedResponse(response, abort);
669
+ const { source, size } = await readBoundedResponse(response, abort);
602
670
  let body;
603
671
  try {
604
672
  body = source.length ? JSON.parse(source) : null;
605
673
  } catch {
606
- throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_JSON_INVALID");
674
+ if (response.status === 201) {
675
+ throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_JSON_INVALID");
676
+ }
677
+ body = null;
607
678
  }
608
- if (response.status === 409 && allowRestart && isExactRestart(body, session.sessionId)) {
679
+ if (response.status === 409 && isExactRestart(body, session.sessionId)) {
680
+ if (!allowRestart) throw new HostedNativeSessionError("NATIVE_SESSION_UNAVAILABLE");
609
681
  assertCurrentSession(session, hostedBinding);
610
682
  const replacement = redirectSession(session, readClock());
611
683
  if (abort.signal.aborted) throw abort.error();
612
684
  return bootstrap(replacement, hostedBinding, abort, false, false);
613
685
  }
614
- if (response.status !== 201) throw statusError(response.status);
686
+ if (response.status !== 201) {
687
+ const failure = statusError(response.status, body, size);
688
+ const delay = retryAfterMs(response.headers, now);
689
+ if (delay !== null) retryAfterDelays.set(failure, delay);
690
+ throw failure;
691
+ }
615
692
  if (!hasNoStore(response.headers.get("cache-control"))) {
616
693
  throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
617
694
  }
@@ -684,6 +761,16 @@ function createHostedNativeSessionController(input) {
684
761
  if (binding.kind === "direct" || !activeSession) {
685
762
  return Object.freeze({ status: "idle", source: "direct", lifecycle });
686
763
  }
764
+ if (terminal) {
765
+ return Object.freeze({
766
+ status: "error",
767
+ source: "hosted",
768
+ lifecycle,
769
+ mapId: binding.mapId,
770
+ sessionId: activeSession.sessionId,
771
+ error: terminal
772
+ });
773
+ }
687
774
  if (activeSession.operation) {
688
775
  return Object.freeze({
689
776
  status: "loading",
@@ -735,6 +822,7 @@ function createHostedNativeSessionController(input) {
735
822
  if (bindingGeneration !== expectedGeneration) {
736
823
  throw new HostedNativeSessionError("NATIVE_SESSION_REPLACED");
737
824
  }
825
+ if (terminal) throw errorFrom(terminal);
738
826
  checkedAt = readClock();
739
827
  if (resolved.authority.clockEpoch === clock.epoch && resolved.authority.validUntil > checkedAt) {
740
828
  admittedAuthorities.set(resolved.authority.public, resolved.authority);
@@ -789,7 +877,7 @@ function createHostedNativeSessionController(input) {
789
877
  }
790
878
  },
791
879
  transportBudget(authority) {
792
- if (disposed || lifecycle !== "foreground") return 0;
880
+ if (disposed || terminal || lifecycle !== "foreground") return 0;
793
881
  const stored = admittedAuthorities.get(authority);
794
882
  if (!stored || stored.bindingGeneration !== bindingGeneration) return 0;
795
883
  try {
@@ -808,6 +896,11 @@ function createHostedNativeSessionController(input) {
808
896
  async resume() {
809
897
  if (disposed) throw new HostedNativeSessionError("NATIVE_SESSION_DISPOSED");
810
898
  lifecycle = "foreground";
899
+ if (terminal) {
900
+ const error = errorFrom(terminal);
901
+ publish(currentSnapshot());
902
+ throw error;
903
+ }
811
904
  if (binding.kind === "direct" || !activeSession) {
812
905
  publish(currentSnapshot());
813
906
  return;
@@ -830,9 +923,27 @@ function createHostedNativeSessionController(input) {
830
923
  bindingGeneration += 1;
831
924
  for (const operation of operations) operation.abort("NATIVE_SESSION_REPLACED");
832
925
  binding = next;
926
+ terminal = null;
927
+ clearRetry();
833
928
  activeSession = next.kind === "hosted" ? createSession(next.surfaceId, readClock()) : null;
834
929
  publish(snapshotIdle(binding, activeSession, lifecycle));
835
930
  },
931
+ retry() {
932
+ if (disposed) throw new HostedNativeSessionError("NATIVE_SESSION_DISPOSED");
933
+ if (!terminal && retryAt === null) return;
934
+ terminal = null;
935
+ clearRetry();
936
+ if (activeSession && !activeSession.operation) activeSession.error = null;
937
+ publish(currentSnapshot());
938
+ },
939
+ retryDelay() {
940
+ if (disposed || terminal || retryAt === null) return null;
941
+ try {
942
+ return Math.max(0, Math.ceil(retryAt - readClock()));
943
+ } catch {
944
+ return null;
945
+ }
946
+ },
836
947
  subscribe(listener) {
837
948
  if (typeof listener !== "function") {
838
949
  throw new HostedNativeSessionError("NATIVE_SESSION_INPUT_INVALID");
@@ -927,7 +1038,8 @@ function parseSuccess(value, binding, expectedSessionId, expectedSurfaceId) {
927
1038
  const issuedAtMs = parseExactIso(value.issuedAt);
928
1039
  const serverTimeMs = parseExactIso(value.serverTime);
929
1040
  const expiresAtMs = parseExactIso(value.expiresAt);
930
- if (issuedAtMs > serverTimeMs + FUTURE_ISSUE_SKEW_MS || expiresAtMs <= serverTimeMs || expiresAtMs <= issuedAtMs || expiresAtMs - issuedAtMs > MAX_GRANT_LIFETIME_MS || expiresAtMs - issuedAtMs > MAX_GRANT_LIFETIME_MS) {
1041
+ 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.
1042
+ expiresAtMs - serverTimeMs > MAX_GRANT_LIFETIME_MS) {
931
1043
  throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
932
1044
  }
933
1045
  const authority = {
@@ -1058,7 +1170,7 @@ async function readBoundedResponse(response, abort) {
1058
1170
  bytes.set(chunk, offset);
1059
1171
  offset += chunk.byteLength;
1060
1172
  }
1061
- return decodeUtf8(bytes);
1173
+ return { source: decodeUtf8(bytes), size };
1062
1174
  }
1063
1175
  function decodeUtf8(bytes) {
1064
1176
  let result = "";
@@ -1106,18 +1218,60 @@ function decodeUtf8(bytes) {
1106
1218
  function hasNoStore(value) {
1107
1219
  return typeof value === "string" && value.split(",").some((part) => part.trim().toLowerCase() === "no-store");
1108
1220
  }
1109
- function statusError(status) {
1221
+ function statusError(status, body, size) {
1110
1222
  if (status === 401) return new HostedNativeSessionError("NATIVE_SESSION_CREDENTIAL_REJECTED");
1111
1223
  if (status === 403) return new HostedNativeSessionError("NATIVE_SESSION_MAP_MISMATCH");
1112
1224
  if (status === 404) return new HostedNativeSessionError("NATIVE_SESSION_MAP_NOT_FOUND");
1113
- if (status === 429) return new HostedNativeSessionError("NATIVE_SESSION_QUOTA_EXCEEDED");
1114
- if (status === 503) return new HostedNativeSessionError("NATIVE_SESSION_UNAVAILABLE");
1115
- return new HostedNativeSessionError("NATIVE_SESSION_REJECTED");
1225
+ if (status === 429) {
1226
+ return new HostedNativeSessionError(
1227
+ isRecord(body) && typeof body.code === "string" && QUOTA_CODES.has(body.code) ? "NATIVE_SESSION_QUOTA_EXCEEDED" : "NATIVE_SESSION_RATE_LIMITED"
1228
+ );
1229
+ }
1230
+ if (status === 408 || status >= 500 && status <= 599) {
1231
+ return new HostedNativeSessionError("NATIVE_SESSION_UNAVAILABLE");
1232
+ }
1233
+ if (status >= 400 && status <= 499) {
1234
+ return new HostedNativeSessionError(
1235
+ "NATIVE_SESSION_REJECTED",
1236
+ size <= SERVICE_CODE_BODY_LIMIT && isRecord(body) && typeof body.code === "string" ? body.code : void 0
1237
+ );
1238
+ }
1239
+ return new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
1240
+ }
1241
+ function safeError(error) {
1242
+ return Object.freeze(
1243
+ error.reason === void 0 ? { code: error.code, kind: error.kind } : { code: error.code, kind: error.kind, reason: error.reason }
1244
+ );
1245
+ }
1246
+ function errorFrom(error) {
1247
+ return new HostedNativeSessionError(error.code, error.reason);
1248
+ }
1249
+ var retryAfterDelays = /* @__PURE__ */ new WeakMap();
1250
+ function retryAfterMs(headers, now) {
1251
+ let value;
1252
+ try {
1253
+ value = headers.get("retry-after");
1254
+ } catch {
1255
+ return null;
1256
+ }
1257
+ if (typeof value !== "string" || value.length > 64) return null;
1258
+ const text = value.trim();
1259
+ let delay;
1260
+ if (RETRY_AFTER_SECONDS.test(text)) delay = Number(text) * 1e3;
1261
+ else {
1262
+ const date = Date.parse(text);
1263
+ if (!Number.isFinite(date)) return null;
1264
+ try {
1265
+ delay = date - readWallClock(now);
1266
+ } catch {
1267
+ return null;
1268
+ }
1269
+ }
1270
+ return Math.min(Math.max(0, delay), RETRY_AFTER_LIMIT_MS);
1116
1271
  }
1117
1272
  function errorKind(code) {
1118
1273
  if (code === "NATIVE_SESSION_DISPOSED" || code === "NATIVE_SESSION_REPLACED") return "cancelled";
1119
- if (code === "NATIVE_SESSION_UNAVAILABLE") return "recoverable";
1120
- return "terminal";
1274
+ return TERMINAL_CODES.has(code) ? "terminal" : "recoverable";
1121
1275
  }
1122
1276
  function normalizeError(error, fallback = "NATIVE_SESSION_RESPONSE_INVALID") {
1123
1277
  return error instanceof HostedNativeSessionError ? error : new HostedNativeSessionError(fallback);
@@ -1140,12 +1294,13 @@ async function cancelResponse(response) {
1140
1294
  } catch {
1141
1295
  }
1142
1296
  }
1143
- var _aborted, _code, _listeners;
1297
+ var _aborted, _code, _reason, _listeners;
1144
1298
  var SessionAbortController = class {
1145
1299
  constructor() {
1146
1300
  __publicField(this, "signal");
1147
1301
  __privateAdd(this, _aborted, false);
1148
1302
  __privateAdd(this, _code, "NATIVE_SESSION_REPLACED");
1303
+ __privateAdd(this, _reason);
1149
1304
  __privateAdd(this, _listeners, /* @__PURE__ */ new Set());
1150
1305
  const isAborted = () => __privateGet(this, _aborted);
1151
1306
  this.signal = Object.freeze({
@@ -1160,9 +1315,10 @@ var SessionAbortController = class {
1160
1315
  }
1161
1316
  });
1162
1317
  }
1163
- abort(code) {
1318
+ abort(code, reason) {
1164
1319
  if (__privateGet(this, _aborted)) return;
1165
1320
  __privateSet(this, _code, code);
1321
+ __privateSet(this, _reason, reason);
1166
1322
  __privateSet(this, _aborted, true);
1167
1323
  for (const listener of [...__privateGet(this, _listeners)]) {
1168
1324
  try {
@@ -1173,11 +1329,12 @@ var SessionAbortController = class {
1173
1329
  __privateGet(this, _listeners).clear();
1174
1330
  }
1175
1331
  error() {
1176
- return new HostedNativeSessionError(__privateGet(this, _code));
1332
+ return new HostedNativeSessionError(__privateGet(this, _code), __privateGet(this, _reason));
1177
1333
  }
1178
1334
  };
1179
1335
  _aborted = new WeakMap();
1180
1336
  _code = new WeakMap();
1337
+ _reason = new WeakMap();
1181
1338
  _listeners = new WeakMap();
1182
1339
  function raceWithAbort(promise, controller) {
1183
1340
  if (controller.signal.aborted) return Promise.reject(controller.error());
@@ -1416,6 +1573,8 @@ function createNativeAdmissionOwner(options) {
1416
1573
  try {
1417
1574
  resources = normalizeNativeResources(input.resources);
1418
1575
  if (input.onRetired !== void 0 && typeof input.onRetired !== "function") throw cancel();
1576
+ if (input.onSessionError !== void 0 && typeof input.onSessionError !== "function")
1577
+ throw cancel();
1419
1578
  } catch {
1420
1579
  throw cancel("NATIVE_ADMISSION_INVALID");
1421
1580
  }
@@ -1476,6 +1635,17 @@ function createNativeAdmissionOwner(options) {
1476
1635
  controller,
1477
1636
  handle: void 0
1478
1637
  };
1638
+ let reported = null;
1639
+ const releaseSession = controller.subscribe((state) => {
1640
+ const code = state.status === "error" ? state.error.code : null;
1641
+ if (!context.live || code === reported) return;
1642
+ reported = code;
1643
+ if (state.status !== "error") return;
1644
+ try {
1645
+ input.onSessionError?.(Object.freeze({ ...state.error }));
1646
+ } catch {
1647
+ }
1648
+ });
1479
1649
  context.handle = Object.freeze({
1480
1650
  context: context.id,
1481
1651
  generation: context.generation,
@@ -1505,9 +1675,26 @@ function createNativeAdmissionOwner(options) {
1505
1675
  discriminate: catalog.discriminate,
1506
1676
  discriminateForTest: catalog.discriminate,
1507
1677
  extendResources: catalog.extend,
1678
+ retry() {
1679
+ if (!context.live || disposed) return;
1680
+ try {
1681
+ controller.retry?.();
1682
+ } catch {
1683
+ }
1684
+ },
1685
+ retryDelay() {
1686
+ if (!context.live || disposed) return null;
1687
+ try {
1688
+ const delay = controller.retryDelay?.() ?? null;
1689
+ return delay !== null && Number.isSafeInteger(delay) && delay >= 0 ? delay : null;
1690
+ } catch {
1691
+ return null;
1692
+ }
1693
+ },
1508
1694
  retire() {
1509
1695
  if (retirement) return retirement;
1510
1696
  context.live = false;
1697
+ releaseSession();
1511
1698
  context.batch = null;
1512
1699
  context.tickets.clear();
1513
1700
  catalog.retire();
@@ -1,4 +1,4 @@
1
- import { z as MapThemeSelection, f as MapColorScheme } from '../contract-By3URNxS.js';
1
+ import { B as MapThemeSelection, g as MapColorScheme } from '../contract-BWT1jxqg.js';
2
2
  import '@maplibre/maplibre-react-native';
3
3
  import 'react';
4
4
  import 'react-native';
@@ -42,12 +42,14 @@ type HostedNativeSessionFetchResponse = Readonly<{
42
42
  }> | null;
43
43
  }>;
44
44
  type HostedNativeSessionFetch = (url: string, init: HostedNativeSessionFetchInit) => Promise<HostedNativeSessionFetchResponse>;
45
- type HostedNativeSessionErrorCode = 'NATIVE_SESSION_INPUT_INVALID' | 'NATIVE_SESSION_RESPONSE_TOO_LARGE' | 'NATIVE_SESSION_RESPONSE_UTF8_INVALID' | 'NATIVE_SESSION_RESPONSE_JSON_INVALID' | 'NATIVE_SESSION_RESPONSE_INVALID' | 'NATIVE_SESSION_CREDENTIAL_REJECTED' | 'NATIVE_SESSION_MAP_MISMATCH' | 'NATIVE_SESSION_MAP_NOT_FOUND' | 'NATIVE_SESSION_QUOTA_EXCEEDED' | 'NATIVE_SESSION_REJECTED' | 'NATIVE_SESSION_UNAVAILABLE' | 'NATIVE_SESSION_REPLACED' | 'NATIVE_SESSION_DISPOSED';
45
+ type HostedNativeSessionErrorCode = 'NATIVE_SESSION_INPUT_INVALID' | 'NATIVE_SESSION_RESPONSE_TOO_LARGE' | 'NATIVE_SESSION_RESPONSE_UTF8_INVALID' | 'NATIVE_SESSION_RESPONSE_JSON_INVALID' | 'NATIVE_SESSION_RESPONSE_INVALID' | 'NATIVE_SESSION_CREDENTIAL_REJECTED' | 'NATIVE_SESSION_MAP_MISMATCH' | 'NATIVE_SESSION_MAP_NOT_FOUND' | 'NATIVE_SESSION_QUOTA_EXCEEDED' | 'NATIVE_SESSION_REJECTED' | 'NATIVE_SESSION_RATE_LIMITED' | 'NATIVE_SESSION_UNAVAILABLE' | 'NATIVE_SESSION_REPLACED' | 'NATIVE_SESSION_DISPOSED';
46
46
  type HostedNativeSessionErrorKind = 'terminal' | 'recoverable' | 'cancelled';
47
47
  declare class HostedNativeSessionError extends Error {
48
48
  readonly code: HostedNativeSessionErrorCode;
49
49
  readonly kind: HostedNativeSessionErrorKind;
50
- constructor(code: HostedNativeSessionErrorCode);
50
+ /** For `NATIVE_SESSION_REJECTED`, the service's code when its answer carried one. */
51
+ readonly reason?: string;
52
+ constructor(code: HostedNativeSessionErrorCode, reason?: string);
51
53
  }
52
54
  type HostedNativeSessionAuthority = Readonly<{
53
55
  grant: string;
@@ -71,6 +73,7 @@ type HostedNativeResourcePolicy = Readonly<Pick<HostedNativeSessionAuthority, 'm
71
73
  type SafeError = Readonly<{
72
74
  code: HostedNativeSessionErrorCode;
73
75
  kind: HostedNativeSessionErrorKind;
76
+ reason?: string;
74
77
  }>;
75
78
  type Lifecycle = 'foreground' | 'background';
76
79
  type HostedNativeSessionState = Readonly<{
@@ -112,6 +115,10 @@ type HostedNativeSessionController = Readonly<{
112
115
  background(): void;
113
116
  resume(): Promise<void>;
114
117
  replaceBinding(binding: HostedNativeSessionBinding): void;
118
+ /** Clears a terminal stop and any retry delay; the next acquisition bootstraps again. */
119
+ retry?(): void;
120
+ /** Milliseconds until a bootstrap may follow a recoverable failure, or null without one. */
121
+ retryDelay?(): number | null;
115
122
  subscribe(listener: (state: HostedNativeSessionState) => void): () => void;
116
123
  dispose(): void;
117
124
  }>;