@tileflow/react-native 0.0.0-bootstrap.0 → 0.1.0-alpha.0
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 +216 -4
- package/TileflowNativeAdmission.podspec +34 -0
- package/android/build.gradle +43 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionBootstrap.kt +144 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionBootstrapOkHttpNetwork.kt +89 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionCatalog.kt +137 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionDocumentScope.kt +22 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionEngine.kt +335 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionModuleProvider.kt +68 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionOkHttpNetwork.kt +86 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionProviderLease.kt +56 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionTypes.kt +85 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionUrl.kt +100 -0
- package/android/src/main/java/dev/tileflow/reactnative/HandlerAdmissionScheduler.kt +16 -0
- package/android/src/main/java/dev/tileflow/reactnative/MobileConfiguration.kt +95 -0
- package/android/src/main/java/dev/tileflow/reactnative/MobileConfigurationResources.kt +43 -0
- package/android/src/main/java/dev/tileflow/reactnative/NativeDocumentOkHttpNetwork.kt +101 -0
- package/android/src/main/java/dev/tileflow/reactnative/NativeDocumentRegistry.kt +136 -0
- package/android/src/main/java/dev/tileflow/reactnative/NativeSurfaceState.kt +115 -0
- package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeAdmissionModule.kt +234 -0
- package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeAdmissionPackage.kt +16 -0
- package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeConfigurationModule.kt +43 -0
- package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeDocumentsModule.kt +80 -0
- package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeSurfaceModule.kt +277 -0
- package/android/src/main/res/raw/tileflow_configuration_keep.xml +3 -0
- package/dist/contract-DWs-lDPo.d.ts +144 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +6989 -0
- package/dist/internal/camera.d.ts +65 -0
- package/dist/internal/camera.js +325 -0
- package/dist/internal/hosted-binding.d.ts +18 -0
- package/dist/internal/hosted-binding.js +277 -0
- package/dist/internal/initial-view.d.ts +11 -0
- package/dist/internal/initial-view.js +8 -0
- package/dist/internal/interactions.d.ts +76 -0
- package/dist/internal/interactions.js +794 -0
- package/dist/internal/native-admission-bridge.d.ts +29 -0
- package/dist/internal/native-admission-bridge.js +351 -0
- package/dist/internal/native-admission.d.ts +78 -0
- package/dist/internal/native-admission.js +1614 -0
- package/dist/internal/native-appearance.d.ts +21 -0
- package/dist/internal/native-appearance.js +110 -0
- package/dist/internal/native-configuration-bridge.d.ts +9 -0
- package/dist/internal/native-configuration-bridge.js +343 -0
- package/dist/internal/session.d.ts +125 -0
- package/dist/internal/session.js +887 -0
- package/dist/internal/source-state.d.ts +11 -0
- package/dist/internal/source-state.js +26 -0
- package/dist/native-admission-contract-k7LKE1vW.d.ts +89 -0
- package/docs/native-admission.md +108 -0
- package/docs/native-configuration.md +210 -0
- package/docs/native-interactions.md +161 -0
- package/docs/native-location.md +148 -0
- package/ios/TFAdmissionBootstrap.h +17 -0
- package/ios/TFAdmissionBootstrap.mm +168 -0
- package/ios/TFAdmissionCatalog.h +13 -0
- package/ios/TFAdmissionCatalog.mm +193 -0
- package/ios/TFAdmissionDocumentScope.h +7 -0
- package/ios/TFAdmissionDocumentScope.mm +45 -0
- package/ios/TFAdmissionEngine.h +57 -0
- package/ios/TFAdmissionEngine.mm +371 -0
- package/ios/TFAdmissionInstallation.h +27 -0
- package/ios/TFAdmissionInstallation.mm +117 -0
- package/ios/TFAdmissionNetwork.h +16 -0
- package/ios/TFAdmissionNetwork.mm +268 -0
- package/ios/TFAdmissionURL.mm +116 -0
- package/ios/TFDocumentBounds.h +9 -0
- package/ios/TFDocumentBounds.mm +19 -0
- package/ios/TFMobileConfiguration.h +23 -0
- package/ios/TFMobileConfiguration.mm +139 -0
- package/ios/TFNativeAdmissionURLProtocol.mm +69 -0
- package/ios/TFNativeDocumentRegistry.h +28 -0
- package/ios/TFNativeDocumentRegistry.mm +193 -0
- package/ios/TFNativeDocumentURL.h +6 -0
- package/ios/TFNativeSurfaceState.h +28 -0
- package/ios/TFNativeSurfaceState.mm +128 -0
- package/ios/TileflowNativeAdmission.h +9 -0
- package/ios/TileflowNativeAdmission.mm +184 -0
- package/ios/TileflowNativeConfiguration.mm +41 -0
- package/ios/TileflowNativeDocuments.h +12 -0
- package/ios/TileflowNativeDocuments.mm +105 -0
- package/ios/TileflowNativeSurface.mm +403 -0
- package/ios/tileflow_native_admission.rb +45 -0
- package/package.json +51 -7
- package/react-native.config.cjs +13 -0
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __typeError = (msg) => {
|
|
3
|
+
throw TypeError(msg);
|
|
4
|
+
};
|
|
5
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
6
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
7
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
8
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
9
|
+
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
10
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
11
|
+
|
|
12
|
+
// src/session-controller.ts
|
|
13
|
+
import { resolveTileflowNativeManifestUrl } from "@tileflow/core/native";
|
|
14
|
+
var RESPONSE_BYTE_LIMIT = 65536;
|
|
15
|
+
var GRANT_CHARACTER_LIMIT = 24576;
|
|
16
|
+
var MAX_TILESETS = 18;
|
|
17
|
+
var MAX_GRANT_LIFETIME_MS = 15 * 60 * 1e3;
|
|
18
|
+
var REFRESH_MARGIN_MS = 3e4;
|
|
19
|
+
var ROTATION_AGE_MS = 6 * 60 * 60 * 1e3;
|
|
20
|
+
var ROTATION_REQUEST_LIMIT = 1e4;
|
|
21
|
+
var FUTURE_ISSUE_SKEW_MS = 3e4;
|
|
22
|
+
var MOBILE_CREDENTIAL = /^tf_public_[0-9a-f]{48}$/u;
|
|
23
|
+
var MAP_ID = /^map_[A-Za-z0-9_-]{16}$/u;
|
|
24
|
+
var CREDENTIAL_ID = /^key_[A-Za-z0-9_-]{8,80}$/u;
|
|
25
|
+
var COMPACT_ID = /^[A-Za-z0-9._:-]{1,255}$/u;
|
|
26
|
+
var SURFACE_ID = /^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/u;
|
|
27
|
+
var NATIVE_GRANT = /^tf_native_v1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/u;
|
|
28
|
+
var EXACT_ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u;
|
|
29
|
+
var ALLOWED_SCOPES = /* @__PURE__ */ new Set([
|
|
30
|
+
"style",
|
|
31
|
+
"tilejson",
|
|
32
|
+
"tile",
|
|
33
|
+
"sprite",
|
|
34
|
+
"glyph",
|
|
35
|
+
"font"
|
|
36
|
+
]);
|
|
37
|
+
var SUCCESS_KEYS = /* @__PURE__ */ new Set([
|
|
38
|
+
"ok",
|
|
39
|
+
"kind",
|
|
40
|
+
"version",
|
|
41
|
+
"usageMode",
|
|
42
|
+
"counted",
|
|
43
|
+
"grant",
|
|
44
|
+
"mapId",
|
|
45
|
+
"sessionId",
|
|
46
|
+
"surfaceId",
|
|
47
|
+
"credentialId",
|
|
48
|
+
"credentialRevision",
|
|
49
|
+
"deliveryPolicyRevision",
|
|
50
|
+
"issuedAt",
|
|
51
|
+
"serverTime",
|
|
52
|
+
"expiresAt",
|
|
53
|
+
"meterMode",
|
|
54
|
+
"disposition",
|
|
55
|
+
"resourceOrigins",
|
|
56
|
+
"resourceScopes",
|
|
57
|
+
"tilesetIds"
|
|
58
|
+
]);
|
|
59
|
+
var RESTART_KEYS = /* @__PURE__ */ new Set(["code", "error", "retryWithNewSession", "sessionId"]);
|
|
60
|
+
var ERROR_MESSAGES = {
|
|
61
|
+
NATIVE_SESSION_INPUT_INVALID: "Native session configuration is invalid.",
|
|
62
|
+
NATIVE_SESSION_RESPONSE_TOO_LARGE: "Native session response exceeded its byte limit.",
|
|
63
|
+
NATIVE_SESSION_RESPONSE_UTF8_INVALID: "Native session response was not valid UTF-8.",
|
|
64
|
+
NATIVE_SESSION_RESPONSE_JSON_INVALID: "Native session response was not valid JSON.",
|
|
65
|
+
NATIVE_SESSION_RESPONSE_INVALID: "Native session response did not satisfy the expected contract.",
|
|
66
|
+
NATIVE_SESSION_CREDENTIAL_REJECTED: "Native session credential was rejected.",
|
|
67
|
+
NATIVE_SESSION_MAP_MISMATCH: "Native session authority does not match the requested Map.",
|
|
68
|
+
NATIVE_SESSION_MAP_NOT_FOUND: "Native session Map was not found.",
|
|
69
|
+
NATIVE_SESSION_QUOTA_EXCEEDED: "Native session quota was exceeded.",
|
|
70
|
+
NATIVE_SESSION_REJECTED: "Native session request was rejected.",
|
|
71
|
+
NATIVE_SESSION_UNAVAILABLE: "Native session service is unavailable.",
|
|
72
|
+
NATIVE_SESSION_REPLACED: "Native session binding was replaced.",
|
|
73
|
+
NATIVE_SESSION_DISPOSED: "Native session controller is disposed."
|
|
74
|
+
};
|
|
75
|
+
var HostedNativeSessionError = class extends Error {
|
|
76
|
+
constructor(code) {
|
|
77
|
+
super(ERROR_MESSAGES[code]);
|
|
78
|
+
__publicField(this, "code");
|
|
79
|
+
__publicField(this, "kind");
|
|
80
|
+
this.name = "HostedNativeSessionError";
|
|
81
|
+
this.code = code;
|
|
82
|
+
this.kind = errorKind(code);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
function createHostedNativeSessionController(input) {
|
|
86
|
+
if (!isRecord(input) || typeof input.fetch !== "function" || typeof input.now !== "function" || typeof input.sessionIdFactory !== "function") {
|
|
87
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_INPUT_INVALID");
|
|
88
|
+
}
|
|
89
|
+
const fetch = input.fetch;
|
|
90
|
+
const now = input.now;
|
|
91
|
+
const sessionIdFactory = input.sessionIdFactory;
|
|
92
|
+
const issuedSessionIds = /* @__PURE__ */ new Set();
|
|
93
|
+
let binding = normalizeBinding(input.binding);
|
|
94
|
+
let lifecycle = "foreground";
|
|
95
|
+
let disposed = false;
|
|
96
|
+
let bindingGeneration = 0;
|
|
97
|
+
const admittedAuthorities = /* @__PURE__ */ new WeakMap();
|
|
98
|
+
const clock = { lastWall: readWallClock(now), logical: 0, epoch: 0 };
|
|
99
|
+
let activeSession = binding.kind === "hosted" ? createSession(binding.surfaceId, 0) : null;
|
|
100
|
+
let state = snapshotIdle(binding, activeSession, lifecycle);
|
|
101
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
102
|
+
const operations = /* @__PURE__ */ new Set();
|
|
103
|
+
function readClock() {
|
|
104
|
+
const wall = readWallClock(now);
|
|
105
|
+
if (wall < clock.lastWall) clock.epoch += 1;
|
|
106
|
+
else clock.logical += wall - clock.lastWall;
|
|
107
|
+
clock.lastWall = wall;
|
|
108
|
+
return clock.logical;
|
|
109
|
+
}
|
|
110
|
+
function bindingIsDirect() {
|
|
111
|
+
return binding.kind === "direct";
|
|
112
|
+
}
|
|
113
|
+
function createSession(surfaceId, startedAt) {
|
|
114
|
+
let sessionId;
|
|
115
|
+
try {
|
|
116
|
+
sessionId = sessionIdFactory();
|
|
117
|
+
} catch {
|
|
118
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_INPUT_INVALID");
|
|
119
|
+
}
|
|
120
|
+
if (typeof sessionId !== "string" || !COMPACT_ID.test(sessionId) || secretShaped(sessionId) || issuedSessionIds.has(sessionId)) {
|
|
121
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_INPUT_INVALID");
|
|
122
|
+
}
|
|
123
|
+
issuedSessionIds.add(sessionId);
|
|
124
|
+
return {
|
|
125
|
+
authority: null,
|
|
126
|
+
error: null,
|
|
127
|
+
lastServerTime: null,
|
|
128
|
+
operation: null,
|
|
129
|
+
pendingAdmissions: 0,
|
|
130
|
+
redirect: null,
|
|
131
|
+
requestCount: 0,
|
|
132
|
+
requestedSurfaceId: surfaceId,
|
|
133
|
+
serverStartedAt: null,
|
|
134
|
+
sessionId,
|
|
135
|
+
startedAt
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function publish(next) {
|
|
139
|
+
if (disposed && next.status !== "disposed") return;
|
|
140
|
+
state = next;
|
|
141
|
+
for (const listener of [...listeners]) {
|
|
142
|
+
try {
|
|
143
|
+
listener(next);
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function publishSession(session, status, error) {
|
|
149
|
+
if (disposed || activeSession !== session || binding.kind !== "hosted") return;
|
|
150
|
+
if (status === "ready") {
|
|
151
|
+
if (!session.authority) return;
|
|
152
|
+
const authority = session.authority.public;
|
|
153
|
+
publish(
|
|
154
|
+
Object.freeze({
|
|
155
|
+
status: "ready",
|
|
156
|
+
source: "hosted",
|
|
157
|
+
lifecycle,
|
|
158
|
+
mapId: binding.mapId,
|
|
159
|
+
sessionId: session.sessionId,
|
|
160
|
+
surfaceId: authority.surfaceId,
|
|
161
|
+
credentialId: authority.credentialId,
|
|
162
|
+
credentialRevision: authority.credentialRevision,
|
|
163
|
+
deliveryPolicyRevision: authority.deliveryPolicyRevision
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (status === "error") {
|
|
169
|
+
if (!error) return;
|
|
170
|
+
publish(
|
|
171
|
+
Object.freeze({
|
|
172
|
+
status: "error",
|
|
173
|
+
source: "hosted",
|
|
174
|
+
lifecycle,
|
|
175
|
+
mapId: binding.mapId,
|
|
176
|
+
sessionId: session.sessionId,
|
|
177
|
+
error: Object.freeze({ ...error })
|
|
178
|
+
})
|
|
179
|
+
);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
publish(
|
|
183
|
+
Object.freeze({
|
|
184
|
+
status,
|
|
185
|
+
source: "hosted",
|
|
186
|
+
lifecycle,
|
|
187
|
+
mapId: binding.mapId,
|
|
188
|
+
sessionId: session.sessionId
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
function rotateIfRequired(at) {
|
|
193
|
+
if (!activeSession || binding.kind !== "hosted") return;
|
|
194
|
+
if (at - activeSession.startedAt < ROTATION_AGE_MS && activeSession.requestCount < ROTATION_REQUEST_LIMIT) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const previous = activeSession;
|
|
198
|
+
activeSession = createSession(
|
|
199
|
+
previous.authority?.public.surfaceId ?? previous.requestedSurfaceId,
|
|
200
|
+
at
|
|
201
|
+
);
|
|
202
|
+
publishSession(activeSession, "idle");
|
|
203
|
+
}
|
|
204
|
+
function redirectSession(session, at) {
|
|
205
|
+
const replacement = createSession(
|
|
206
|
+
session.authority?.public.surfaceId ?? session.requestedSurfaceId,
|
|
207
|
+
at
|
|
208
|
+
);
|
|
209
|
+
replacement.requestCount = session.pendingAdmissions;
|
|
210
|
+
replacement.operation = session.operation;
|
|
211
|
+
session.redirect = replacement;
|
|
212
|
+
activeSession = replacement;
|
|
213
|
+
publishSession(replacement, "loading");
|
|
214
|
+
return replacement;
|
|
215
|
+
}
|
|
216
|
+
function latestSession(session) {
|
|
217
|
+
const seen = /* @__PURE__ */ new Set();
|
|
218
|
+
let current = session;
|
|
219
|
+
while (current.redirect && !seen.has(current.redirect)) {
|
|
220
|
+
seen.add(current);
|
|
221
|
+
current = current.redirect;
|
|
222
|
+
}
|
|
223
|
+
return current;
|
|
224
|
+
}
|
|
225
|
+
async function ensureAuthority(session, at) {
|
|
226
|
+
if (session.authority && session.authority.clockEpoch === clock.epoch && session.authority.validUntil - at > REFRESH_MARGIN_MS) {
|
|
227
|
+
return { authority: session.authority, session };
|
|
228
|
+
}
|
|
229
|
+
if (session.operation) return session.operation.promise;
|
|
230
|
+
if (binding.kind !== "hosted") {
|
|
231
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_REPLACED");
|
|
232
|
+
}
|
|
233
|
+
const operationBinding = binding;
|
|
234
|
+
const abort = new SessionAbortController();
|
|
235
|
+
operations.add(abort);
|
|
236
|
+
const operation = {
|
|
237
|
+
abort,
|
|
238
|
+
promise: Promise.resolve().then(async () => {
|
|
239
|
+
session.error = null;
|
|
240
|
+
publishSession(session, "loading");
|
|
241
|
+
if (abort.signal.aborted) throw abort.error();
|
|
242
|
+
try {
|
|
243
|
+
const result = await bootstrap(session, operationBinding, abort, true, true);
|
|
244
|
+
if (abort.signal.aborted) throw abort.error();
|
|
245
|
+
result.session.authority = result.authority;
|
|
246
|
+
result.session.error = null;
|
|
247
|
+
if (activeSession === result.session) {
|
|
248
|
+
publishSession(result.session, "ready");
|
|
249
|
+
if (abort.signal.aborted) throw abort.error();
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
} catch (error) {
|
|
253
|
+
const normalized = abort.signal.aborted ? abort.error() : normalizeError(error);
|
|
254
|
+
const target = latestSession(session);
|
|
255
|
+
if (normalized.code !== "NATIVE_SESSION_REPLACED" && normalized.code !== "NATIVE_SESSION_DISPOSED") {
|
|
256
|
+
target.error = Object.freeze({ code: normalized.code, kind: normalized.kind });
|
|
257
|
+
publishSession(target, "error", target.error);
|
|
258
|
+
}
|
|
259
|
+
throw normalized;
|
|
260
|
+
} finally {
|
|
261
|
+
operations.delete(abort);
|
|
262
|
+
let current = session;
|
|
263
|
+
const seen = /* @__PURE__ */ new Set();
|
|
264
|
+
while (current && !seen.has(current)) {
|
|
265
|
+
seen.add(current);
|
|
266
|
+
if (current.operation === operation) current.operation = null;
|
|
267
|
+
current = current.redirect;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
};
|
|
272
|
+
session.operation = operation;
|
|
273
|
+
return operation.promise;
|
|
274
|
+
}
|
|
275
|
+
async function bootstrap(session, hostedBinding, abort, allowRestart, allowServerAgeRotation) {
|
|
276
|
+
const requestStartedAt = readClock();
|
|
277
|
+
const requestClockEpoch = clock.epoch;
|
|
278
|
+
const requestBindingGeneration = bindingGeneration;
|
|
279
|
+
if (abort.signal.aborted) throw abort.error();
|
|
280
|
+
const expectedSurfaceId = session.authority?.public.surfaceId ?? session.requestedSurfaceId;
|
|
281
|
+
const response = await fetchResponse(hostedBinding, session, expectedSurfaceId, abort);
|
|
282
|
+
const source = await readBoundedResponse(response, abort);
|
|
283
|
+
let body;
|
|
284
|
+
try {
|
|
285
|
+
body = source.length ? JSON.parse(source) : null;
|
|
286
|
+
} catch {
|
|
287
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_JSON_INVALID");
|
|
288
|
+
}
|
|
289
|
+
if (response.status === 409 && allowRestart && isExactRestart(body, session.sessionId)) {
|
|
290
|
+
assertCurrentSession(session, hostedBinding);
|
|
291
|
+
const replacement = redirectSession(session, readClock());
|
|
292
|
+
if (abort.signal.aborted) throw abort.error();
|
|
293
|
+
return bootstrap(replacement, hostedBinding, abort, false, false);
|
|
294
|
+
}
|
|
295
|
+
if (response.status !== 201) throw statusError(response.status);
|
|
296
|
+
if (!hasNoStore(response.headers.get("cache-control"))) {
|
|
297
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
|
|
298
|
+
}
|
|
299
|
+
const payload = parseSuccess(body, hostedBinding, session.sessionId, expectedSurfaceId);
|
|
300
|
+
if (session.lastServerTime !== null && payload.serverTimeMs < session.lastServerTime) {
|
|
301
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
|
|
302
|
+
}
|
|
303
|
+
if (allowServerAgeRotation && session.serverStartedAt !== null && payload.serverTimeMs - session.serverStartedAt >= ROTATION_AGE_MS) {
|
|
304
|
+
assertCurrentSession(session, hostedBinding);
|
|
305
|
+
const replacement = redirectSession(session, readClock());
|
|
306
|
+
if (abort.signal.aborted) throw abort.error();
|
|
307
|
+
return bootstrap(replacement, hostedBinding, abort, false, false);
|
|
308
|
+
}
|
|
309
|
+
session.serverStartedAt ?? (session.serverStartedAt = payload.serverTimeMs);
|
|
310
|
+
session.lastServerTime = payload.serverTimeMs;
|
|
311
|
+
const ttl = payload.expiresAtMs - payload.serverTimeMs;
|
|
312
|
+
return {
|
|
313
|
+
authority: Object.freeze({
|
|
314
|
+
public: payload.authority,
|
|
315
|
+
validUntil: requestStartedAt + ttl,
|
|
316
|
+
clockEpoch: requestClockEpoch,
|
|
317
|
+
bindingGeneration: requestBindingGeneration
|
|
318
|
+
}),
|
|
319
|
+
session
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function assertCurrentSession(session, expectedBinding) {
|
|
323
|
+
if (activeSession !== session || binding.kind !== "hosted" || !sameHostedBinding(binding, expectedBinding)) {
|
|
324
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_REPLACED");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function fetchResponse(hostedBinding, session, surfaceId, abort) {
|
|
328
|
+
const body = JSON.stringify({
|
|
329
|
+
mapId: hostedBinding.mapId,
|
|
330
|
+
sessionId: session.sessionId,
|
|
331
|
+
surfaceId
|
|
332
|
+
});
|
|
333
|
+
const init = Object.freeze({
|
|
334
|
+
body,
|
|
335
|
+
credentials: "omit",
|
|
336
|
+
headers: Object.freeze({
|
|
337
|
+
"Content-Type": "application/json",
|
|
338
|
+
"X-Tileflow-Mobile-Client": hostedBinding.credential
|
|
339
|
+
}),
|
|
340
|
+
method: "POST",
|
|
341
|
+
signal: abort.signal
|
|
342
|
+
});
|
|
343
|
+
let pending;
|
|
344
|
+
try {
|
|
345
|
+
pending = Promise.resolve(fetch(`${hostedBinding.apiOrigin}/v1/sessions/start`, init));
|
|
346
|
+
} catch {
|
|
347
|
+
if (abort.signal.aborted) throw abort.error();
|
|
348
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_UNAVAILABLE");
|
|
349
|
+
}
|
|
350
|
+
pending.then(
|
|
351
|
+
(response) => {
|
|
352
|
+
if (abort.signal.aborted) void cancelResponse(response);
|
|
353
|
+
},
|
|
354
|
+
() => void 0
|
|
355
|
+
);
|
|
356
|
+
try {
|
|
357
|
+
return await raceWithAbort(pending, abort);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
if (abort.signal.aborted) throw abort.error();
|
|
360
|
+
throw normalizeError(error, "NATIVE_SESSION_UNAVAILABLE");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function currentSnapshot() {
|
|
364
|
+
if (disposed) return Object.freeze({ status: "disposed", lifecycle: "disposed" });
|
|
365
|
+
if (binding.kind === "direct" || !activeSession) {
|
|
366
|
+
return Object.freeze({ status: "idle", source: "direct", lifecycle });
|
|
367
|
+
}
|
|
368
|
+
if (activeSession.operation) {
|
|
369
|
+
return Object.freeze({
|
|
370
|
+
status: "loading",
|
|
371
|
+
source: "hosted",
|
|
372
|
+
lifecycle,
|
|
373
|
+
mapId: binding.mapId,
|
|
374
|
+
sessionId: activeSession.sessionId
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
if (activeSession.error) {
|
|
378
|
+
return Object.freeze({
|
|
379
|
+
status: "error",
|
|
380
|
+
source: "hosted",
|
|
381
|
+
lifecycle,
|
|
382
|
+
mapId: binding.mapId,
|
|
383
|
+
sessionId: activeSession.sessionId,
|
|
384
|
+
error: Object.freeze({ ...activeSession.error })
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
if (activeSession.authority) {
|
|
388
|
+
const authority = activeSession.authority.public;
|
|
389
|
+
return Object.freeze({
|
|
390
|
+
status: "ready",
|
|
391
|
+
source: "hosted",
|
|
392
|
+
lifecycle,
|
|
393
|
+
mapId: binding.mapId,
|
|
394
|
+
sessionId: activeSession.sessionId,
|
|
395
|
+
surfaceId: authority.surfaceId,
|
|
396
|
+
credentialId: authority.credentialId,
|
|
397
|
+
credentialRevision: authority.credentialRevision,
|
|
398
|
+
deliveryPolicyRevision: authority.deliveryPolicyRevision
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
return Object.freeze({
|
|
402
|
+
status: "idle",
|
|
403
|
+
source: "hosted",
|
|
404
|
+
lifecycle,
|
|
405
|
+
mapId: binding.mapId,
|
|
406
|
+
sessionId: activeSession.sessionId
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
async function acquireAuthority(session, at) {
|
|
410
|
+
const expectedGeneration = bindingGeneration;
|
|
411
|
+
let candidate = session;
|
|
412
|
+
let checkedAt = at;
|
|
413
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
414
|
+
const resolved = await ensureAuthority(candidate, checkedAt);
|
|
415
|
+
if (disposed) throw new HostedNativeSessionError("NATIVE_SESSION_DISPOSED");
|
|
416
|
+
if (bindingGeneration !== expectedGeneration) {
|
|
417
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_REPLACED");
|
|
418
|
+
}
|
|
419
|
+
checkedAt = readClock();
|
|
420
|
+
if (resolved.authority.clockEpoch === clock.epoch && resolved.authority.validUntil > checkedAt) {
|
|
421
|
+
admittedAuthorities.set(resolved.authority.public, resolved.authority);
|
|
422
|
+
return resolved.authority.public;
|
|
423
|
+
}
|
|
424
|
+
resolved.session.authority = null;
|
|
425
|
+
candidate = resolved.session;
|
|
426
|
+
}
|
|
427
|
+
const error = new HostedNativeSessionError("NATIVE_SESSION_UNAVAILABLE");
|
|
428
|
+
candidate.error = Object.freeze({ code: error.code, kind: error.kind });
|
|
429
|
+
publishSession(candidate, "error", candidate.error);
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
return Object.freeze({
|
|
433
|
+
get state() {
|
|
434
|
+
return state;
|
|
435
|
+
},
|
|
436
|
+
async prepare() {
|
|
437
|
+
if (disposed) throw new HostedNativeSessionError("NATIVE_SESSION_DISPOSED");
|
|
438
|
+
if (bindingIsDirect()) return null;
|
|
439
|
+
if (lifecycle !== "foreground")
|
|
440
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_UNAVAILABLE");
|
|
441
|
+
const at = readClock();
|
|
442
|
+
rotateIfRequired(at);
|
|
443
|
+
if (disposed) throw new HostedNativeSessionError("NATIVE_SESSION_DISPOSED");
|
|
444
|
+
if (bindingIsDirect() || !activeSession)
|
|
445
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_REPLACED");
|
|
446
|
+
const authority = await acquireAuthority(activeSession, at);
|
|
447
|
+
if (lifecycle !== "foreground")
|
|
448
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_UNAVAILABLE");
|
|
449
|
+
return Object.freeze({
|
|
450
|
+
mapId: authority.mapId,
|
|
451
|
+
resourceOrigins: Object.freeze([...authority.resourceOrigins]),
|
|
452
|
+
resourceScopes: Object.freeze([...authority.resourceScopes]),
|
|
453
|
+
tilesetIds: Object.freeze([...authority.tilesetIds])
|
|
454
|
+
});
|
|
455
|
+
},
|
|
456
|
+
async acquire() {
|
|
457
|
+
if (disposed) throw new HostedNativeSessionError("NATIVE_SESSION_DISPOSED");
|
|
458
|
+
if (bindingIsDirect()) return null;
|
|
459
|
+
const at = readClock();
|
|
460
|
+
rotateIfRequired(at);
|
|
461
|
+
if (disposed) throw new HostedNativeSessionError("NATIVE_SESSION_DISPOSED");
|
|
462
|
+
if (bindingIsDirect() || !activeSession) return null;
|
|
463
|
+
const session = activeSession;
|
|
464
|
+
session.requestCount += 1;
|
|
465
|
+
session.pendingAdmissions += 1;
|
|
466
|
+
try {
|
|
467
|
+
return await acquireAuthority(session, at);
|
|
468
|
+
} finally {
|
|
469
|
+
session.pendingAdmissions = Math.max(0, session.pendingAdmissions - 1);
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
transportBudget(authority) {
|
|
473
|
+
if (disposed || lifecycle !== "foreground") return 0;
|
|
474
|
+
const stored = admittedAuthorities.get(authority);
|
|
475
|
+
if (!stored || stored.bindingGeneration !== bindingGeneration) return 0;
|
|
476
|
+
try {
|
|
477
|
+
const at = readClock();
|
|
478
|
+
if (stored.clockEpoch !== clock.epoch) return 0;
|
|
479
|
+
return Math.max(0, Math.floor(stored.validUntil - at));
|
|
480
|
+
} catch {
|
|
481
|
+
return 0;
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
background() {
|
|
485
|
+
if (disposed) return;
|
|
486
|
+
lifecycle = "background";
|
|
487
|
+
publish(currentSnapshot());
|
|
488
|
+
},
|
|
489
|
+
async resume() {
|
|
490
|
+
if (disposed) throw new HostedNativeSessionError("NATIVE_SESSION_DISPOSED");
|
|
491
|
+
lifecycle = "foreground";
|
|
492
|
+
if (binding.kind === "direct" || !activeSession) {
|
|
493
|
+
publish(currentSnapshot());
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const previous = activeSession;
|
|
497
|
+
const hadAuthority = Boolean(previous.authority || previous.operation);
|
|
498
|
+
const at = readClock();
|
|
499
|
+
rotateIfRequired(at);
|
|
500
|
+
if (disposed) throw new HostedNativeSessionError("NATIVE_SESSION_DISPOSED");
|
|
501
|
+
publish(currentSnapshot());
|
|
502
|
+
if (!hadAuthority || !activeSession) return;
|
|
503
|
+
if (activeSession !== previous || !activeSession.authority || activeSession.authority.clockEpoch !== clock.epoch || activeSession.authority.validUntil - at <= REFRESH_MARGIN_MS) {
|
|
504
|
+
await acquireAuthority(activeSession, at);
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
replaceBinding(nextBinding) {
|
|
508
|
+
if (disposed) throw new HostedNativeSessionError("NATIVE_SESSION_DISPOSED");
|
|
509
|
+
const next = normalizeBinding(nextBinding);
|
|
510
|
+
if (sameBinding(binding, next)) return;
|
|
511
|
+
bindingGeneration += 1;
|
|
512
|
+
for (const operation of operations) operation.abort("NATIVE_SESSION_REPLACED");
|
|
513
|
+
binding = next;
|
|
514
|
+
activeSession = next.kind === "hosted" ? createSession(next.surfaceId, readClock()) : null;
|
|
515
|
+
publish(snapshotIdle(binding, activeSession, lifecycle));
|
|
516
|
+
},
|
|
517
|
+
subscribe(listener) {
|
|
518
|
+
if (typeof listener !== "function") {
|
|
519
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_INPUT_INVALID");
|
|
520
|
+
}
|
|
521
|
+
if (disposed) return () => void 0;
|
|
522
|
+
listeners.add(listener);
|
|
523
|
+
let released = false;
|
|
524
|
+
return () => {
|
|
525
|
+
if (released) return;
|
|
526
|
+
released = true;
|
|
527
|
+
listeners.delete(listener);
|
|
528
|
+
};
|
|
529
|
+
},
|
|
530
|
+
dispose() {
|
|
531
|
+
if (disposed) return;
|
|
532
|
+
disposed = true;
|
|
533
|
+
bindingGeneration += 1;
|
|
534
|
+
for (const operation of operations) operation.abort("NATIVE_SESSION_DISPOSED");
|
|
535
|
+
operations.clear();
|
|
536
|
+
activeSession = null;
|
|
537
|
+
binding = Object.freeze({ kind: "direct" });
|
|
538
|
+
publish(Object.freeze({ status: "disposed", lifecycle: "disposed" }));
|
|
539
|
+
listeners.clear();
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
function normalizeBinding(value) {
|
|
544
|
+
try {
|
|
545
|
+
if (!isRecord(value)) throw new Error();
|
|
546
|
+
if (value.kind === "direct") {
|
|
547
|
+
if (Object.keys(value).length !== 1) throw new Error();
|
|
548
|
+
return Object.freeze({ kind: "direct" });
|
|
549
|
+
}
|
|
550
|
+
if (value.kind !== "hosted" || Object.keys(value).some(
|
|
551
|
+
(key) => !["kind", "apiOrigin", "credential", "mapId", "surfaceId"].includes(key)
|
|
552
|
+
) || typeof value.credential !== "string" || !MOBILE_CREDENTIAL.test(value.credential) || typeof value.mapId !== "string" || !MAP_ID.test(value.mapId)) {
|
|
553
|
+
throw new Error();
|
|
554
|
+
}
|
|
555
|
+
return Object.freeze({
|
|
556
|
+
kind: "hosted",
|
|
557
|
+
apiOrigin: canonicalOrigin(value.apiOrigin),
|
|
558
|
+
credential: value.credential,
|
|
559
|
+
mapId: value.mapId,
|
|
560
|
+
surfaceId: normalizeSurface(value.surfaceId)
|
|
561
|
+
});
|
|
562
|
+
} catch (error) {
|
|
563
|
+
if (error instanceof HostedNativeSessionError) throw error;
|
|
564
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_INPUT_INVALID");
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function canonicalOrigin(value) {
|
|
568
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 2048 || value !== value.trim()) {
|
|
569
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_INPUT_INVALID");
|
|
570
|
+
}
|
|
571
|
+
try {
|
|
572
|
+
const resolved = resolveTileflowNativeManifestUrl(value);
|
|
573
|
+
if (!resolved.endsWith("/")) throw new Error();
|
|
574
|
+
const origin = resolved.slice(0, -1);
|
|
575
|
+
if (value !== origin) throw new Error();
|
|
576
|
+
return origin;
|
|
577
|
+
} catch {
|
|
578
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_INPUT_INVALID");
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
function normalizeSurface(value) {
|
|
582
|
+
return typeof value === "string" && SURFACE_ID.test(value) && !secretShaped(value) ? value : "default";
|
|
583
|
+
}
|
|
584
|
+
function secretShaped(value) {
|
|
585
|
+
return MOBILE_CREDENTIAL.test(value) || value.startsWith("tf_native_");
|
|
586
|
+
}
|
|
587
|
+
function sameHostedBinding(left, right) {
|
|
588
|
+
return left.apiOrigin === right.apiOrigin && left.credential === right.credential && left.mapId === right.mapId;
|
|
589
|
+
}
|
|
590
|
+
function sameBinding(left, right) {
|
|
591
|
+
if (left.kind !== right.kind) return false;
|
|
592
|
+
if (left.kind === "direct") return true;
|
|
593
|
+
return right.kind === "hosted" && sameHostedBinding(left, right);
|
|
594
|
+
}
|
|
595
|
+
function snapshotIdle(binding, session, lifecycle) {
|
|
596
|
+
return binding.kind === "direct" || !session ? Object.freeze({ status: "idle", source: "direct", lifecycle }) : Object.freeze({
|
|
597
|
+
status: "idle",
|
|
598
|
+
source: "hosted",
|
|
599
|
+
lifecycle,
|
|
600
|
+
mapId: binding.mapId,
|
|
601
|
+
sessionId: session.sessionId
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
function parseSuccess(value, binding, expectedSessionId, expectedSurfaceId) {
|
|
605
|
+
if (!isRecord(value) || Object.keys(value).length !== SUCCESS_KEYS.size || Object.keys(value).some((key) => !SUCCESS_KEYS.has(key)) || value.ok !== true || value.kind !== "native" || value.version !== 1 || value.usageMode !== "session" || value.counted !== false || value.mapId !== binding.mapId || value.sessionId !== expectedSessionId || value.surfaceId !== expectedSurfaceId || typeof value.surfaceId !== "string" || !SURFACE_ID.test(value.surfaceId) || secretShaped(value.surfaceId) || typeof value.credentialId !== "string" || !CREDENTIAL_ID.test(value.credentialId) || !positiveRevision(value.credentialRevision) || !positiveRevision(value.deliveryPolicyRevision) || typeof value.grant !== "string" || value.grant.length === 0 || value.grant.length > GRANT_CHARACTER_LIMIT || !NATIVE_GRANT.test(value.grant) || !["disabled", "shadow", "enforced"].includes(String(value.meterMode)) || !["unmetered", "ordinary", "unbilled_fail_open"].includes(String(value.disposition)) || value.disposition === "unmetered" && value.meterMode !== "disabled") {
|
|
606
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
|
|
607
|
+
}
|
|
608
|
+
const issuedAtMs = parseExactIso(value.issuedAt);
|
|
609
|
+
const serverTimeMs = parseExactIso(value.serverTime);
|
|
610
|
+
const expiresAtMs = parseExactIso(value.expiresAt);
|
|
611
|
+
if (issuedAtMs > serverTimeMs + FUTURE_ISSUE_SKEW_MS || expiresAtMs <= serverTimeMs || expiresAtMs <= issuedAtMs || expiresAtMs - issuedAtMs > MAX_GRANT_LIFETIME_MS || expiresAtMs - issuedAtMs > MAX_GRANT_LIFETIME_MS) {
|
|
612
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
|
|
613
|
+
}
|
|
614
|
+
const authority = {
|
|
615
|
+
mapId: value.mapId,
|
|
616
|
+
sessionId: value.sessionId,
|
|
617
|
+
surfaceId: value.surfaceId,
|
|
618
|
+
credentialId: value.credentialId,
|
|
619
|
+
credentialRevision: value.credentialRevision,
|
|
620
|
+
deliveryPolicyRevision: value.deliveryPolicyRevision,
|
|
621
|
+
issuedAt: value.issuedAt,
|
|
622
|
+
serverTime: value.serverTime,
|
|
623
|
+
expiresAt: value.expiresAt,
|
|
624
|
+
meterMode: value.meterMode,
|
|
625
|
+
disposition: value.disposition,
|
|
626
|
+
resourceOrigins: Object.freeze(parseOrigins(value.resourceOrigins)),
|
|
627
|
+
resourceScopes: Object.freeze(parseScopes(value.resourceScopes)),
|
|
628
|
+
tilesetIds: Object.freeze(parseTilesets(value.tilesetIds))
|
|
629
|
+
};
|
|
630
|
+
const withGrant = Object.defineProperty(authority, "grant", {
|
|
631
|
+
value: value.grant,
|
|
632
|
+
enumerable: false,
|
|
633
|
+
configurable: false,
|
|
634
|
+
writable: false
|
|
635
|
+
});
|
|
636
|
+
return { authority: Object.freeze(withGrant), serverTimeMs, expiresAtMs };
|
|
637
|
+
}
|
|
638
|
+
function parseOrigins(value) {
|
|
639
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 2) {
|
|
640
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
|
|
641
|
+
}
|
|
642
|
+
const result = [];
|
|
643
|
+
for (const item of value) {
|
|
644
|
+
if (typeof item !== "string") {
|
|
645
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
|
|
646
|
+
}
|
|
647
|
+
let origin;
|
|
648
|
+
try {
|
|
649
|
+
const resolved = resolveTileflowNativeManifestUrl(item);
|
|
650
|
+
if (!resolved.endsWith("/")) throw new Error();
|
|
651
|
+
origin = resolved.slice(0, -1);
|
|
652
|
+
if (item !== origin) throw new Error();
|
|
653
|
+
} catch {
|
|
654
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
|
|
655
|
+
}
|
|
656
|
+
if (result.includes(origin)) {
|
|
657
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
|
|
658
|
+
}
|
|
659
|
+
result.push(origin);
|
|
660
|
+
}
|
|
661
|
+
return result;
|
|
662
|
+
}
|
|
663
|
+
function parseScopes(value) {
|
|
664
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > ALLOWED_SCOPES.size) {
|
|
665
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
|
|
666
|
+
}
|
|
667
|
+
const result = [];
|
|
668
|
+
for (const item of value) {
|
|
669
|
+
if (typeof item !== "string" || !ALLOWED_SCOPES.has(item) || result.includes(item)) {
|
|
670
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
|
|
671
|
+
}
|
|
672
|
+
result.push(item);
|
|
673
|
+
}
|
|
674
|
+
return result;
|
|
675
|
+
}
|
|
676
|
+
function parseTilesets(value) {
|
|
677
|
+
if (!Array.isArray(value) || value.length > MAX_TILESETS) {
|
|
678
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
|
|
679
|
+
}
|
|
680
|
+
const result = [];
|
|
681
|
+
for (const item of value) {
|
|
682
|
+
if (typeof item !== "string" || !COMPACT_ID.test(item) || result.includes(item)) {
|
|
683
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
|
|
684
|
+
}
|
|
685
|
+
result.push(item);
|
|
686
|
+
}
|
|
687
|
+
return result;
|
|
688
|
+
}
|
|
689
|
+
function parseExactIso(value) {
|
|
690
|
+
if (typeof value !== "string" || !EXACT_ISO.test(value)) {
|
|
691
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
|
|
692
|
+
}
|
|
693
|
+
const parsed = Date.parse(value);
|
|
694
|
+
if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== value) {
|
|
695
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
|
|
696
|
+
}
|
|
697
|
+
return parsed;
|
|
698
|
+
}
|
|
699
|
+
function positiveRevision(value) {
|
|
700
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
701
|
+
}
|
|
702
|
+
function isExactRestart(value, expectedSessionId) {
|
|
703
|
+
return isRecord(value) && Object.keys(value).length === RESTART_KEYS.size && Object.keys(value).every((key) => RESTART_KEYS.has(key)) && value.code === "COMMERCIAL_SESSION_RESTART_REQUIRED" && typeof value.error === "string" && value.error.length > 0 && value.error.length <= 500 && value.retryWithNewSession === true && value.sessionId === expectedSessionId;
|
|
704
|
+
}
|
|
705
|
+
async function readBoundedResponse(response, abort) {
|
|
706
|
+
let reader;
|
|
707
|
+
try {
|
|
708
|
+
if (!response || !Number.isSafeInteger(response.status) || !response.headers || typeof response.headers.get !== "function" || !response.body || typeof response.body.getReader !== "function") {
|
|
709
|
+
throw new Error();
|
|
710
|
+
}
|
|
711
|
+
reader = response.body.getReader();
|
|
712
|
+
} catch {
|
|
713
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
|
|
714
|
+
}
|
|
715
|
+
const chunks = [];
|
|
716
|
+
let size = 0;
|
|
717
|
+
try {
|
|
718
|
+
for (; ; ) {
|
|
719
|
+
if (abort.signal.aborted) throw abort.error();
|
|
720
|
+
const next = await raceWithAbort(Promise.resolve(reader.read()), abort);
|
|
721
|
+
if (next.done) break;
|
|
722
|
+
if (!(next.value instanceof Uint8Array)) {
|
|
723
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
|
|
724
|
+
}
|
|
725
|
+
size += next.value.byteLength;
|
|
726
|
+
if (size > RESPONSE_BYTE_LIMIT) {
|
|
727
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_TOO_LARGE");
|
|
728
|
+
}
|
|
729
|
+
chunks.push(next.value.slice());
|
|
730
|
+
}
|
|
731
|
+
} catch (error) {
|
|
732
|
+
void Promise.resolve(reader.cancel()).catch(() => void 0);
|
|
733
|
+
if (abort.signal.aborted) throw abort.error();
|
|
734
|
+
throw normalizeError(error, "NATIVE_SESSION_UNAVAILABLE");
|
|
735
|
+
}
|
|
736
|
+
const bytes = new Uint8Array(size);
|
|
737
|
+
let offset = 0;
|
|
738
|
+
for (const chunk of chunks) {
|
|
739
|
+
bytes.set(chunk, offset);
|
|
740
|
+
offset += chunk.byteLength;
|
|
741
|
+
}
|
|
742
|
+
return decodeUtf8(bytes);
|
|
743
|
+
}
|
|
744
|
+
function decodeUtf8(bytes) {
|
|
745
|
+
let result = "";
|
|
746
|
+
for (let index = 0; index < bytes.length; ) {
|
|
747
|
+
const first = bytes[index++];
|
|
748
|
+
let codePoint;
|
|
749
|
+
let remaining;
|
|
750
|
+
let minimum;
|
|
751
|
+
if (first <= 127) {
|
|
752
|
+
codePoint = first;
|
|
753
|
+
remaining = 0;
|
|
754
|
+
minimum = 0;
|
|
755
|
+
} else if (first >= 194 && first <= 223) {
|
|
756
|
+
codePoint = first & 31;
|
|
757
|
+
remaining = 1;
|
|
758
|
+
minimum = 128;
|
|
759
|
+
} else if (first >= 224 && first <= 239) {
|
|
760
|
+
codePoint = first & 15;
|
|
761
|
+
remaining = 2;
|
|
762
|
+
minimum = 2048;
|
|
763
|
+
} else if (first >= 240 && first <= 244) {
|
|
764
|
+
codePoint = first & 7;
|
|
765
|
+
remaining = 3;
|
|
766
|
+
minimum = 65536;
|
|
767
|
+
} else {
|
|
768
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_UTF8_INVALID");
|
|
769
|
+
}
|
|
770
|
+
if (index + remaining > bytes.length) {
|
|
771
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_UTF8_INVALID");
|
|
772
|
+
}
|
|
773
|
+
for (let count = 0; count < remaining; count += 1) {
|
|
774
|
+
const byte = bytes[index++];
|
|
775
|
+
if ((byte & 192) !== 128) {
|
|
776
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_UTF8_INVALID");
|
|
777
|
+
}
|
|
778
|
+
codePoint = codePoint << 6 | byte & 63;
|
|
779
|
+
}
|
|
780
|
+
if (codePoint < minimum || codePoint > 1114111 || codePoint >= 55296 && codePoint <= 57343) {
|
|
781
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_UTF8_INVALID");
|
|
782
|
+
}
|
|
783
|
+
result += String.fromCodePoint(codePoint);
|
|
784
|
+
}
|
|
785
|
+
return result;
|
|
786
|
+
}
|
|
787
|
+
function hasNoStore(value) {
|
|
788
|
+
return typeof value === "string" && value.split(",").some((part) => part.trim().toLowerCase() === "no-store");
|
|
789
|
+
}
|
|
790
|
+
function statusError(status) {
|
|
791
|
+
if (status === 401) return new HostedNativeSessionError("NATIVE_SESSION_CREDENTIAL_REJECTED");
|
|
792
|
+
if (status === 403) return new HostedNativeSessionError("NATIVE_SESSION_MAP_MISMATCH");
|
|
793
|
+
if (status === 404) return new HostedNativeSessionError("NATIVE_SESSION_MAP_NOT_FOUND");
|
|
794
|
+
if (status === 429) return new HostedNativeSessionError("NATIVE_SESSION_QUOTA_EXCEEDED");
|
|
795
|
+
if (status === 503) return new HostedNativeSessionError("NATIVE_SESSION_UNAVAILABLE");
|
|
796
|
+
return new HostedNativeSessionError("NATIVE_SESSION_REJECTED");
|
|
797
|
+
}
|
|
798
|
+
function errorKind(code) {
|
|
799
|
+
if (code === "NATIVE_SESSION_DISPOSED" || code === "NATIVE_SESSION_REPLACED") return "cancelled";
|
|
800
|
+
if (code === "NATIVE_SESSION_UNAVAILABLE") return "recoverable";
|
|
801
|
+
return "terminal";
|
|
802
|
+
}
|
|
803
|
+
function normalizeError(error, fallback = "NATIVE_SESSION_RESPONSE_INVALID") {
|
|
804
|
+
return error instanceof HostedNativeSessionError ? error : new HostedNativeSessionError(fallback);
|
|
805
|
+
}
|
|
806
|
+
function readWallClock(now) {
|
|
807
|
+
try {
|
|
808
|
+
const value = now();
|
|
809
|
+
if (!(value instanceof Date) || !Number.isFinite(value.getTime())) throw new Error();
|
|
810
|
+
return value.getTime();
|
|
811
|
+
} catch {
|
|
812
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_INPUT_INVALID");
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
function isRecord(value) {
|
|
816
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
817
|
+
}
|
|
818
|
+
async function cancelResponse(response) {
|
|
819
|
+
try {
|
|
820
|
+
await response.body?.getReader().cancel();
|
|
821
|
+
} catch {
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
var _aborted, _code, _listeners;
|
|
825
|
+
var SessionAbortController = class {
|
|
826
|
+
constructor() {
|
|
827
|
+
__publicField(this, "signal");
|
|
828
|
+
__privateAdd(this, _aborted, false);
|
|
829
|
+
__privateAdd(this, _code, "NATIVE_SESSION_REPLACED");
|
|
830
|
+
__privateAdd(this, _listeners, /* @__PURE__ */ new Set());
|
|
831
|
+
const isAborted = () => __privateGet(this, _aborted);
|
|
832
|
+
this.signal = Object.freeze({
|
|
833
|
+
get aborted() {
|
|
834
|
+
return isAborted();
|
|
835
|
+
},
|
|
836
|
+
addEventListener: (type, listener, _options) => {
|
|
837
|
+
if (type === "abort" && !__privateGet(this, _aborted)) __privateGet(this, _listeners).add(listener);
|
|
838
|
+
},
|
|
839
|
+
removeEventListener: (type, listener) => {
|
|
840
|
+
if (type === "abort") __privateGet(this, _listeners).delete(listener);
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
abort(code) {
|
|
845
|
+
if (__privateGet(this, _aborted)) return;
|
|
846
|
+
__privateSet(this, _code, code);
|
|
847
|
+
__privateSet(this, _aborted, true);
|
|
848
|
+
for (const listener of [...__privateGet(this, _listeners)]) {
|
|
849
|
+
try {
|
|
850
|
+
listener();
|
|
851
|
+
} catch {
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
__privateGet(this, _listeners).clear();
|
|
855
|
+
}
|
|
856
|
+
error() {
|
|
857
|
+
return new HostedNativeSessionError(__privateGet(this, _code));
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
_aborted = new WeakMap();
|
|
861
|
+
_code = new WeakMap();
|
|
862
|
+
_listeners = new WeakMap();
|
|
863
|
+
function raceWithAbort(promise, controller) {
|
|
864
|
+
if (controller.signal.aborted) return Promise.reject(controller.error());
|
|
865
|
+
return new Promise((resolve, reject) => {
|
|
866
|
+
const onAbort = () => {
|
|
867
|
+
cleanup();
|
|
868
|
+
reject(controller.error());
|
|
869
|
+
};
|
|
870
|
+
const cleanup = () => controller.signal.removeEventListener("abort", onAbort);
|
|
871
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
872
|
+
promise.then(
|
|
873
|
+
(value) => {
|
|
874
|
+
cleanup();
|
|
875
|
+
resolve(value);
|
|
876
|
+
},
|
|
877
|
+
() => {
|
|
878
|
+
cleanup();
|
|
879
|
+
reject(new HostedNativeSessionError("NATIVE_SESSION_UNAVAILABLE"));
|
|
880
|
+
}
|
|
881
|
+
);
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
export {
|
|
885
|
+
HostedNativeSessionError,
|
|
886
|
+
createHostedNativeSessionController
|
|
887
|
+
};
|