@tileflow/react-native 0.1.0-alpha.6 → 0.1.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -14
- 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-By3URNxS.d.ts → contract-BWT1jxqg.d.ts} +44 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +503 -55
- 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 +15 -1
- package/dist/internal/native-admission.js +208 -21
- package/dist/internal/native-appearance.d.ts +1 -1
- package/dist/internal/session.d.ts +9 -2
- package/dist/internal/session.js +178 -21
- package/dist/internal/source-state.d.ts +1 -1
- package/ios/TFAdmissionBootstrap.mm +12 -6
- package/ios/TFAdmissionEngine.mm +4 -2
- package/package.json +3 -4
- package/docs/native-admission.md +0 -108
- package/docs/native-configuration.md +0 -210
- package/docs/native-interactions.md +0 -161
- package/docs/native-location.md +0 -148
|
@@ -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-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
|
-
|
|
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,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 =
|
|
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
|
-
|
|
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 &&
|
|
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)
|
|
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 ||
|
|
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)
|
|
1114
|
-
|
|
1115
|
-
|
|
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
|
-
|
|
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();
|
|
@@ -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
|
-
|
|
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
|
}>;
|