@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/README.md +66 -8
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionBootstrap.kt +7 -3
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionBootstrapOkHttpNetwork.kt +3 -1
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionEngine.kt +4 -2
- package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeAdmissionModule.kt +1 -1
- package/dist/{contract-Bp-Im7tj.d.ts → contract-BaK24QZD.d.ts} +40 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +464 -48
- package/dist/internal/camera.d.ts +1 -1
- package/dist/internal/initial-view.d.ts +1 -1
- package/dist/internal/native-admission-bridge.js +8 -3
- package/dist/internal/native-admission.d.ts +14 -1
- package/dist/internal/native-admission.js +173 -12
- package/dist/internal/native-appearance.d.ts +1 -1
- package/dist/internal/session.d.ts +5 -1
- package/dist/internal/session.js +143 -12
- package/dist/internal/source-state.d.ts +1 -1
- package/docs/native-admission.md +17 -1
- package/ios/TFAdmissionBootstrap.mm +12 -6
- package/ios/TFAdmissionEngine.mm +4 -2
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { M as MapView, a as MapViewChangeEvent, b as MapInitialViewInputs, c as MapCameraProps } from '../contract-
|
|
1
|
+
import { M as MapView, a as MapViewChangeEvent, b as MapInitialViewInputs, c as MapCameraProps } from '../contract-BaK24QZD.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
|
-
|
|
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
|
-
|
|
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,11 @@ 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
|
+
}>) => void;
|
|
10
15
|
}>;
|
|
11
16
|
type NativeMapAdmission = Readonly<{
|
|
12
17
|
context: string;
|
|
@@ -25,6 +30,10 @@ type NativeMapAdmission = Readonly<{
|
|
|
25
30
|
extendResources(resources: readonly NativeAdmissionResource[]): Promise<Readonly<{
|
|
26
31
|
resources: number;
|
|
27
32
|
}>>;
|
|
33
|
+
/** Lets a stopped or delayed session bootstrap again on its next acquisition. */
|
|
34
|
+
retry(): void;
|
|
35
|
+
/** Milliseconds until the session may bootstrap after a recoverable failure, or null. */
|
|
36
|
+
retryDelay(): number | null;
|
|
28
37
|
retire(): Promise<Readonly<{
|
|
29
38
|
retired: true;
|
|
30
39
|
}>>;
|
|
@@ -63,6 +72,10 @@ declare function createNativeAdmissionOwner(options: {
|
|
|
63
72
|
extendResources(resources: readonly NativeAdmissionResource[]): Promise<Readonly<{
|
|
64
73
|
resources: number;
|
|
65
74
|
}>>;
|
|
75
|
+
/** Lets a stopped or delayed session bootstrap again on its next acquisition. */
|
|
76
|
+
retry(): void;
|
|
77
|
+
/** Milliseconds until the session may bootstrap after a recoverable failure, or null. */
|
|
78
|
+
retryDelay(): number | null;
|
|
66
79
|
retire(): Promise<Readonly<{
|
|
67
80
|
retired: true;
|
|
68
81
|
}>>;
|
|
@@ -338,6 +338,11 @@ 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"]);
|
|
341
346
|
var MOBILE_CREDENTIAL = /^tf_public_[0-9a-f]{48}$/u;
|
|
342
347
|
var MAP_ID = /^map_[A-Za-z0-9_-]{16}$/u;
|
|
343
348
|
var CREDENTIAL_ID = /^key_[A-Za-z0-9_-]{8,80}$/u;
|
|
@@ -376,6 +381,13 @@ var SUCCESS_KEYS = /* @__PURE__ */ new Set([
|
|
|
376
381
|
"tilesetIds"
|
|
377
382
|
]);
|
|
378
383
|
var RESTART_KEYS = /* @__PURE__ */ new Set(["code", "error", "retryWithNewSession", "sessionId"]);
|
|
384
|
+
var TERMINAL_CODES = /* @__PURE__ */ new Set([
|
|
385
|
+
"NATIVE_SESSION_INPUT_INVALID",
|
|
386
|
+
"NATIVE_SESSION_CREDENTIAL_REJECTED",
|
|
387
|
+
"NATIVE_SESSION_MAP_MISMATCH",
|
|
388
|
+
"NATIVE_SESSION_MAP_NOT_FOUND",
|
|
389
|
+
"NATIVE_SESSION_QUOTA_EXCEEDED"
|
|
390
|
+
]);
|
|
379
391
|
var ERROR_MESSAGES = {
|
|
380
392
|
NATIVE_SESSION_INPUT_INVALID: "Native session configuration is invalid.",
|
|
381
393
|
NATIVE_SESSION_RESPONSE_TOO_LARGE: "Native session response exceeded its byte limit.",
|
|
@@ -387,6 +399,7 @@ var ERROR_MESSAGES = {
|
|
|
387
399
|
NATIVE_SESSION_MAP_NOT_FOUND: "Native session Map was not found.",
|
|
388
400
|
NATIVE_SESSION_QUOTA_EXCEEDED: "Native session quota was exceeded.",
|
|
389
401
|
NATIVE_SESSION_REJECTED: "Native session request was rejected.",
|
|
402
|
+
NATIVE_SESSION_RATE_LIMITED: "Native session requests are rate limited.",
|
|
390
403
|
NATIVE_SESSION_UNAVAILABLE: "Native session service is unavailable.",
|
|
391
404
|
NATIVE_SESSION_REPLACED: "Native session binding was replaced.",
|
|
392
405
|
NATIVE_SESSION_DISPOSED: "Native session controller is disposed."
|
|
@@ -508,7 +521,29 @@ function createHostedNativeSessionController(input) {
|
|
|
508
521
|
})
|
|
509
522
|
);
|
|
510
523
|
}
|
|
524
|
+
let terminal = null;
|
|
525
|
+
let failures = 0;
|
|
526
|
+
let retryAt = null;
|
|
527
|
+
let retryError = null;
|
|
528
|
+
function clearRetry() {
|
|
529
|
+
failures = 0;
|
|
530
|
+
retryAt = null;
|
|
531
|
+
retryError = null;
|
|
532
|
+
}
|
|
533
|
+
function delayRetry(error) {
|
|
534
|
+
let at;
|
|
535
|
+
try {
|
|
536
|
+
at = readClock();
|
|
537
|
+
} catch {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (retryAt === null || at >= retryAt) failures = Math.min(failures + 1, 16);
|
|
541
|
+
const backoff = Math.min(RETRY_INITIAL_MS * 2 ** (failures - 1), RETRY_LIMIT_MS);
|
|
542
|
+
retryAt = Math.max(retryAt ?? at, at + Math.max(backoff, retryAfterDelays.get(error) ?? 0));
|
|
543
|
+
retryError = Object.freeze({ code: error.code, kind: error.kind });
|
|
544
|
+
}
|
|
511
545
|
function rotateIfRequired(at) {
|
|
546
|
+
if (terminal) throw new HostedNativeSessionError(terminal.code);
|
|
512
547
|
if (!activeSession || binding.kind !== "hosted") return;
|
|
513
548
|
if (at - activeSession.startedAt < ROTATION_AGE_MS && activeSession.requestCount < ROTATION_REQUEST_LIMIT) {
|
|
514
549
|
return;
|
|
@@ -542,6 +577,7 @@ function createHostedNativeSessionController(input) {
|
|
|
542
577
|
return current;
|
|
543
578
|
}
|
|
544
579
|
async function ensureAuthority(session, at) {
|
|
580
|
+
if (terminal) throw new HostedNativeSessionError(terminal.code);
|
|
545
581
|
if (session.authority && session.authority.clockEpoch === clock.epoch && session.authority.validUntil - at > REFRESH_MARGIN_MS) {
|
|
546
582
|
return { authority: session.authority, session };
|
|
547
583
|
}
|
|
@@ -549,20 +585,28 @@ function createHostedNativeSessionController(input) {
|
|
|
549
585
|
if (binding.kind !== "hosted") {
|
|
550
586
|
throw new HostedNativeSessionError("NATIVE_SESSION_REPLACED");
|
|
551
587
|
}
|
|
588
|
+
if (retryAt !== null && retryError && at < retryAt) {
|
|
589
|
+
if (session.authority && session.authority.clockEpoch === clock.epoch && session.authority.validUntil > at) {
|
|
590
|
+
return { authority: session.authority, session };
|
|
591
|
+
}
|
|
592
|
+
throw new HostedNativeSessionError(retryError.code);
|
|
593
|
+
}
|
|
552
594
|
const operationBinding = binding;
|
|
553
595
|
const abort = new SessionAbortController();
|
|
554
596
|
operations.add(abort);
|
|
555
597
|
const operation = {
|
|
556
598
|
abort,
|
|
557
599
|
promise: Promise.resolve().then(async () => {
|
|
558
|
-
session.error = null;
|
|
559
|
-
publishSession(session, "loading");
|
|
560
|
-
if (abort.signal.aborted) throw abort.error();
|
|
561
600
|
try {
|
|
601
|
+
if (abort.signal.aborted) throw abort.error();
|
|
602
|
+
session.error = null;
|
|
603
|
+
publishSession(session, "loading");
|
|
604
|
+
if (abort.signal.aborted) throw abort.error();
|
|
562
605
|
const result = await bootstrap(session, operationBinding, abort, true, true);
|
|
563
606
|
if (abort.signal.aborted) throw abort.error();
|
|
564
607
|
result.session.authority = result.authority;
|
|
565
608
|
result.session.error = null;
|
|
609
|
+
if (binding === operationBinding) clearRetry();
|
|
566
610
|
if (activeSession === result.session) {
|
|
567
611
|
publishSession(result.session, "ready");
|
|
568
612
|
if (abort.signal.aborted) throw abort.error();
|
|
@@ -571,6 +615,22 @@ function createHostedNativeSessionController(input) {
|
|
|
571
615
|
} catch (error) {
|
|
572
616
|
const normalized = abort.signal.aborted ? abort.error() : normalizeError(error);
|
|
573
617
|
const target = latestSession(session);
|
|
618
|
+
if (normalized.kind === "terminal" && binding === operationBinding && !terminal) {
|
|
619
|
+
const failure = Object.freeze({ code: normalized.code, kind: normalized.kind });
|
|
620
|
+
terminal = failure;
|
|
621
|
+
const rejectedSession = activeSession;
|
|
622
|
+
if (rejectedSession) {
|
|
623
|
+
rejectedSession.authority = null;
|
|
624
|
+
rejectedSession.error = failure;
|
|
625
|
+
}
|
|
626
|
+
for (const pending of [...operations]) pending.abort(failure.code);
|
|
627
|
+
if (binding === operationBinding && rejectedSession) {
|
|
628
|
+
publishSession(rejectedSession, "error", failure);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (normalized.kind === "recoverable" && binding === operationBinding && !terminal) {
|
|
632
|
+
delayRetry(normalized);
|
|
633
|
+
}
|
|
574
634
|
if (normalized.code !== "NATIVE_SESSION_REPLACED" && normalized.code !== "NATIVE_SESSION_DISPOSED") {
|
|
575
635
|
target.error = Object.freeze({ code: normalized.code, kind: normalized.kind });
|
|
576
636
|
publishSession(target, "error", target.error);
|
|
@@ -603,7 +663,10 @@ function createHostedNativeSessionController(input) {
|
|
|
603
663
|
try {
|
|
604
664
|
body = source.length ? JSON.parse(source) : null;
|
|
605
665
|
} catch {
|
|
606
|
-
|
|
666
|
+
if (response.status === 201) {
|
|
667
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_JSON_INVALID");
|
|
668
|
+
}
|
|
669
|
+
body = null;
|
|
607
670
|
}
|
|
608
671
|
if (response.status === 409 && allowRestart && isExactRestart(body, session.sessionId)) {
|
|
609
672
|
assertCurrentSession(session, hostedBinding);
|
|
@@ -611,7 +674,12 @@ function createHostedNativeSessionController(input) {
|
|
|
611
674
|
if (abort.signal.aborted) throw abort.error();
|
|
612
675
|
return bootstrap(replacement, hostedBinding, abort, false, false);
|
|
613
676
|
}
|
|
614
|
-
if (response.status !== 201)
|
|
677
|
+
if (response.status !== 201) {
|
|
678
|
+
const failure = statusError(response.status, body);
|
|
679
|
+
const delay = retryAfterMs(response.headers, now);
|
|
680
|
+
if (delay !== null) retryAfterDelays.set(failure, delay);
|
|
681
|
+
throw failure;
|
|
682
|
+
}
|
|
615
683
|
if (!hasNoStore(response.headers.get("cache-control"))) {
|
|
616
684
|
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
|
|
617
685
|
}
|
|
@@ -684,6 +752,16 @@ function createHostedNativeSessionController(input) {
|
|
|
684
752
|
if (binding.kind === "direct" || !activeSession) {
|
|
685
753
|
return Object.freeze({ status: "idle", source: "direct", lifecycle });
|
|
686
754
|
}
|
|
755
|
+
if (terminal) {
|
|
756
|
+
return Object.freeze({
|
|
757
|
+
status: "error",
|
|
758
|
+
source: "hosted",
|
|
759
|
+
lifecycle,
|
|
760
|
+
mapId: binding.mapId,
|
|
761
|
+
sessionId: activeSession.sessionId,
|
|
762
|
+
error: terminal
|
|
763
|
+
});
|
|
764
|
+
}
|
|
687
765
|
if (activeSession.operation) {
|
|
688
766
|
return Object.freeze({
|
|
689
767
|
status: "loading",
|
|
@@ -735,6 +813,7 @@ function createHostedNativeSessionController(input) {
|
|
|
735
813
|
if (bindingGeneration !== expectedGeneration) {
|
|
736
814
|
throw new HostedNativeSessionError("NATIVE_SESSION_REPLACED");
|
|
737
815
|
}
|
|
816
|
+
if (terminal) throw new HostedNativeSessionError(terminal.code);
|
|
738
817
|
checkedAt = readClock();
|
|
739
818
|
if (resolved.authority.clockEpoch === clock.epoch && resolved.authority.validUntil > checkedAt) {
|
|
740
819
|
admittedAuthorities.set(resolved.authority.public, resolved.authority);
|
|
@@ -789,7 +868,7 @@ function createHostedNativeSessionController(input) {
|
|
|
789
868
|
}
|
|
790
869
|
},
|
|
791
870
|
transportBudget(authority) {
|
|
792
|
-
if (disposed || lifecycle !== "foreground") return 0;
|
|
871
|
+
if (disposed || terminal || lifecycle !== "foreground") return 0;
|
|
793
872
|
const stored = admittedAuthorities.get(authority);
|
|
794
873
|
if (!stored || stored.bindingGeneration !== bindingGeneration) return 0;
|
|
795
874
|
try {
|
|
@@ -808,6 +887,11 @@ function createHostedNativeSessionController(input) {
|
|
|
808
887
|
async resume() {
|
|
809
888
|
if (disposed) throw new HostedNativeSessionError("NATIVE_SESSION_DISPOSED");
|
|
810
889
|
lifecycle = "foreground";
|
|
890
|
+
if (terminal) {
|
|
891
|
+
const error = new HostedNativeSessionError(terminal.code);
|
|
892
|
+
publish(currentSnapshot());
|
|
893
|
+
throw error;
|
|
894
|
+
}
|
|
811
895
|
if (binding.kind === "direct" || !activeSession) {
|
|
812
896
|
publish(currentSnapshot());
|
|
813
897
|
return;
|
|
@@ -830,9 +914,27 @@ function createHostedNativeSessionController(input) {
|
|
|
830
914
|
bindingGeneration += 1;
|
|
831
915
|
for (const operation of operations) operation.abort("NATIVE_SESSION_REPLACED");
|
|
832
916
|
binding = next;
|
|
917
|
+
terminal = null;
|
|
918
|
+
clearRetry();
|
|
833
919
|
activeSession = next.kind === "hosted" ? createSession(next.surfaceId, readClock()) : null;
|
|
834
920
|
publish(snapshotIdle(binding, activeSession, lifecycle));
|
|
835
921
|
},
|
|
922
|
+
retry() {
|
|
923
|
+
if (disposed) throw new HostedNativeSessionError("NATIVE_SESSION_DISPOSED");
|
|
924
|
+
if (!terminal && retryAt === null) return;
|
|
925
|
+
terminal = null;
|
|
926
|
+
clearRetry();
|
|
927
|
+
if (activeSession && !activeSession.operation) activeSession.error = null;
|
|
928
|
+
publish(currentSnapshot());
|
|
929
|
+
},
|
|
930
|
+
retryDelay() {
|
|
931
|
+
if (disposed || terminal || retryAt === null) return null;
|
|
932
|
+
try {
|
|
933
|
+
return Math.max(0, Math.ceil(retryAt - readClock()));
|
|
934
|
+
} catch {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
},
|
|
836
938
|
subscribe(listener) {
|
|
837
939
|
if (typeof listener !== "function") {
|
|
838
940
|
throw new HostedNativeSessionError("NATIVE_SESSION_INPUT_INVALID");
|
|
@@ -927,7 +1029,8 @@ function parseSuccess(value, binding, expectedSessionId, expectedSurfaceId) {
|
|
|
927
1029
|
const issuedAtMs = parseExactIso(value.issuedAt);
|
|
928
1030
|
const serverTimeMs = parseExactIso(value.serverTime);
|
|
929
1031
|
const expiresAtMs = parseExactIso(value.expiresAt);
|
|
930
|
-
if (issuedAtMs > serverTimeMs + FUTURE_ISSUE_SKEW_MS || expiresAtMs <= serverTimeMs || expiresAtMs <= issuedAtMs || expiresAtMs - issuedAtMs > MAX_GRANT_LIFETIME_MS ||
|
|
1032
|
+
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.
|
|
1033
|
+
expiresAtMs - serverTimeMs > MAX_GRANT_LIFETIME_MS) {
|
|
931
1034
|
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
|
|
932
1035
|
}
|
|
933
1036
|
const authority = {
|
|
@@ -1106,18 +1209,46 @@ function decodeUtf8(bytes) {
|
|
|
1106
1209
|
function hasNoStore(value) {
|
|
1107
1210
|
return typeof value === "string" && value.split(",").some((part) => part.trim().toLowerCase() === "no-store");
|
|
1108
1211
|
}
|
|
1109
|
-
function statusError(status) {
|
|
1212
|
+
function statusError(status, body) {
|
|
1110
1213
|
if (status === 401) return new HostedNativeSessionError("NATIVE_SESSION_CREDENTIAL_REJECTED");
|
|
1111
1214
|
if (status === 403) return new HostedNativeSessionError("NATIVE_SESSION_MAP_MISMATCH");
|
|
1112
1215
|
if (status === 404) return new HostedNativeSessionError("NATIVE_SESSION_MAP_NOT_FOUND");
|
|
1113
|
-
if (status === 429)
|
|
1114
|
-
|
|
1216
|
+
if (status === 429) {
|
|
1217
|
+
return new HostedNativeSessionError(
|
|
1218
|
+
isRecord(body) && typeof body.code === "string" && QUOTA_CODES.has(body.code) ? "NATIVE_SESSION_QUOTA_EXCEEDED" : "NATIVE_SESSION_RATE_LIMITED"
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
if (status === 408 || status >= 500 && status <= 599) {
|
|
1222
|
+
return new HostedNativeSessionError("NATIVE_SESSION_UNAVAILABLE");
|
|
1223
|
+
}
|
|
1115
1224
|
return new HostedNativeSessionError("NATIVE_SESSION_REJECTED");
|
|
1116
1225
|
}
|
|
1226
|
+
var retryAfterDelays = /* @__PURE__ */ new WeakMap();
|
|
1227
|
+
function retryAfterMs(headers, now) {
|
|
1228
|
+
let value;
|
|
1229
|
+
try {
|
|
1230
|
+
value = headers.get("retry-after");
|
|
1231
|
+
} catch {
|
|
1232
|
+
return null;
|
|
1233
|
+
}
|
|
1234
|
+
if (typeof value !== "string" || value.length > 64) return null;
|
|
1235
|
+
const text = value.trim();
|
|
1236
|
+
let delay;
|
|
1237
|
+
if (RETRY_AFTER_SECONDS.test(text)) delay = Number(text) * 1e3;
|
|
1238
|
+
else {
|
|
1239
|
+
const date = Date.parse(text);
|
|
1240
|
+
if (!Number.isFinite(date)) return null;
|
|
1241
|
+
try {
|
|
1242
|
+
delay = date - readWallClock(now);
|
|
1243
|
+
} catch {
|
|
1244
|
+
return null;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
return Math.min(Math.max(0, delay), RETRY_AFTER_LIMIT_MS);
|
|
1248
|
+
}
|
|
1117
1249
|
function errorKind(code) {
|
|
1118
1250
|
if (code === "NATIVE_SESSION_DISPOSED" || code === "NATIVE_SESSION_REPLACED") return "cancelled";
|
|
1119
|
-
|
|
1120
|
-
return "terminal";
|
|
1251
|
+
return TERMINAL_CODES.has(code) ? "terminal" : "recoverable";
|
|
1121
1252
|
}
|
|
1122
1253
|
function normalizeError(error, fallback = "NATIVE_SESSION_RESPONSE_INVALID") {
|
|
1123
1254
|
return error instanceof HostedNativeSessionError ? error : new HostedNativeSessionError(fallback);
|
|
@@ -1416,6 +1547,8 @@ function createNativeAdmissionOwner(options) {
|
|
|
1416
1547
|
try {
|
|
1417
1548
|
resources = normalizeNativeResources(input.resources);
|
|
1418
1549
|
if (input.onRetired !== void 0 && typeof input.onRetired !== "function") throw cancel();
|
|
1550
|
+
if (input.onSessionError !== void 0 && typeof input.onSessionError !== "function")
|
|
1551
|
+
throw cancel();
|
|
1419
1552
|
} catch {
|
|
1420
1553
|
throw cancel("NATIVE_ADMISSION_INVALID");
|
|
1421
1554
|
}
|
|
@@ -1476,6 +1609,17 @@ function createNativeAdmissionOwner(options) {
|
|
|
1476
1609
|
controller,
|
|
1477
1610
|
handle: void 0
|
|
1478
1611
|
};
|
|
1612
|
+
let reported = null;
|
|
1613
|
+
const releaseSession = controller.subscribe((state) => {
|
|
1614
|
+
const code = state.status === "error" ? state.error.code : null;
|
|
1615
|
+
if (!context.live || code === reported) return;
|
|
1616
|
+
reported = code;
|
|
1617
|
+
if (state.status !== "error") return;
|
|
1618
|
+
try {
|
|
1619
|
+
input.onSessionError?.(Object.freeze({ code: state.error.code, kind: state.error.kind }));
|
|
1620
|
+
} catch {
|
|
1621
|
+
}
|
|
1622
|
+
});
|
|
1479
1623
|
context.handle = Object.freeze({
|
|
1480
1624
|
context: context.id,
|
|
1481
1625
|
generation: context.generation,
|
|
@@ -1505,9 +1649,26 @@ function createNativeAdmissionOwner(options) {
|
|
|
1505
1649
|
discriminate: catalog.discriminate,
|
|
1506
1650
|
discriminateForTest: catalog.discriminate,
|
|
1507
1651
|
extendResources: catalog.extend,
|
|
1652
|
+
retry() {
|
|
1653
|
+
if (!context.live || disposed) return;
|
|
1654
|
+
try {
|
|
1655
|
+
controller.retry?.();
|
|
1656
|
+
} catch {
|
|
1657
|
+
}
|
|
1658
|
+
},
|
|
1659
|
+
retryDelay() {
|
|
1660
|
+
if (!context.live || disposed) return null;
|
|
1661
|
+
try {
|
|
1662
|
+
const delay = controller.retryDelay?.() ?? null;
|
|
1663
|
+
return delay !== null && Number.isSafeInteger(delay) && delay >= 0 ? delay : null;
|
|
1664
|
+
} catch {
|
|
1665
|
+
return null;
|
|
1666
|
+
}
|
|
1667
|
+
},
|
|
1508
1668
|
retire() {
|
|
1509
1669
|
if (retirement) return retirement;
|
|
1510
1670
|
context.live = false;
|
|
1671
|
+
releaseSession();
|
|
1511
1672
|
context.batch = null;
|
|
1512
1673
|
context.tickets.clear();
|
|
1513
1674
|
catalog.retire();
|
|
@@ -42,7 +42,7 @@ 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;
|
|
@@ -112,6 +112,10 @@ type HostedNativeSessionController = Readonly<{
|
|
|
112
112
|
background(): void;
|
|
113
113
|
resume(): Promise<void>;
|
|
114
114
|
replaceBinding(binding: HostedNativeSessionBinding): void;
|
|
115
|
+
/** Clears a terminal stop and any retry delay; the next acquisition bootstraps again. */
|
|
116
|
+
retry?(): void;
|
|
117
|
+
/** Milliseconds until a bootstrap may follow a recoverable failure, or null without one. */
|
|
118
|
+
retryDelay?(): number | null;
|
|
115
119
|
subscribe(listener: (state: HostedNativeSessionState) => void): () => void;
|
|
116
120
|
dispose(): void;
|
|
117
121
|
}>;
|