@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
package/dist/internal/session.js
CHANGED
|
@@ -19,6 +19,11 @@ var REFRESH_MARGIN_MS = 3e4;
|
|
|
19
19
|
var ROTATION_AGE_MS = 6 * 60 * 60 * 1e3;
|
|
20
20
|
var ROTATION_REQUEST_LIMIT = 1e4;
|
|
21
21
|
var FUTURE_ISSUE_SKEW_MS = 3e4;
|
|
22
|
+
var RETRY_INITIAL_MS = 1e3;
|
|
23
|
+
var RETRY_LIMIT_MS = 6e4;
|
|
24
|
+
var RETRY_AFTER_LIMIT_MS = 15 * 60 * 1e3;
|
|
25
|
+
var RETRY_AFTER_SECONDS = /^\d{1,10}$/u;
|
|
26
|
+
var QUOTA_CODES = /* @__PURE__ */ new Set(["COMMERCIAL_MAP_LIMIT_REACHED", "COMMERCIAL_SPENDING_LIMIT_REACHED"]);
|
|
22
27
|
var MOBILE_CREDENTIAL = /^tf_public_[0-9a-f]{48}$/u;
|
|
23
28
|
var MAP_ID = /^map_[A-Za-z0-9_-]{16}$/u;
|
|
24
29
|
var CREDENTIAL_ID = /^key_[A-Za-z0-9_-]{8,80}$/u;
|
|
@@ -57,6 +62,13 @@ var SUCCESS_KEYS = /* @__PURE__ */ new Set([
|
|
|
57
62
|
"tilesetIds"
|
|
58
63
|
]);
|
|
59
64
|
var RESTART_KEYS = /* @__PURE__ */ new Set(["code", "error", "retryWithNewSession", "sessionId"]);
|
|
65
|
+
var TERMINAL_CODES = /* @__PURE__ */ new Set([
|
|
66
|
+
"NATIVE_SESSION_INPUT_INVALID",
|
|
67
|
+
"NATIVE_SESSION_CREDENTIAL_REJECTED",
|
|
68
|
+
"NATIVE_SESSION_MAP_MISMATCH",
|
|
69
|
+
"NATIVE_SESSION_MAP_NOT_FOUND",
|
|
70
|
+
"NATIVE_SESSION_QUOTA_EXCEEDED"
|
|
71
|
+
]);
|
|
60
72
|
var ERROR_MESSAGES = {
|
|
61
73
|
NATIVE_SESSION_INPUT_INVALID: "Native session configuration is invalid.",
|
|
62
74
|
NATIVE_SESSION_RESPONSE_TOO_LARGE: "Native session response exceeded its byte limit.",
|
|
@@ -68,6 +80,7 @@ var ERROR_MESSAGES = {
|
|
|
68
80
|
NATIVE_SESSION_MAP_NOT_FOUND: "Native session Map was not found.",
|
|
69
81
|
NATIVE_SESSION_QUOTA_EXCEEDED: "Native session quota was exceeded.",
|
|
70
82
|
NATIVE_SESSION_REJECTED: "Native session request was rejected.",
|
|
83
|
+
NATIVE_SESSION_RATE_LIMITED: "Native session requests are rate limited.",
|
|
71
84
|
NATIVE_SESSION_UNAVAILABLE: "Native session service is unavailable.",
|
|
72
85
|
NATIVE_SESSION_REPLACED: "Native session binding was replaced.",
|
|
73
86
|
NATIVE_SESSION_DISPOSED: "Native session controller is disposed."
|
|
@@ -189,7 +202,29 @@ function createHostedNativeSessionController(input) {
|
|
|
189
202
|
})
|
|
190
203
|
);
|
|
191
204
|
}
|
|
205
|
+
let terminal = null;
|
|
206
|
+
let failures = 0;
|
|
207
|
+
let retryAt = null;
|
|
208
|
+
let retryError = null;
|
|
209
|
+
function clearRetry() {
|
|
210
|
+
failures = 0;
|
|
211
|
+
retryAt = null;
|
|
212
|
+
retryError = null;
|
|
213
|
+
}
|
|
214
|
+
function delayRetry(error) {
|
|
215
|
+
let at;
|
|
216
|
+
try {
|
|
217
|
+
at = readClock();
|
|
218
|
+
} catch {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (retryAt === null || at >= retryAt) failures = Math.min(failures + 1, 16);
|
|
222
|
+
const backoff = Math.min(RETRY_INITIAL_MS * 2 ** (failures - 1), RETRY_LIMIT_MS);
|
|
223
|
+
retryAt = Math.max(retryAt ?? at, at + Math.max(backoff, retryAfterDelays.get(error) ?? 0));
|
|
224
|
+
retryError = Object.freeze({ code: error.code, kind: error.kind });
|
|
225
|
+
}
|
|
192
226
|
function rotateIfRequired(at) {
|
|
227
|
+
if (terminal) throw new HostedNativeSessionError(terminal.code);
|
|
193
228
|
if (!activeSession || binding.kind !== "hosted") return;
|
|
194
229
|
if (at - activeSession.startedAt < ROTATION_AGE_MS && activeSession.requestCount < ROTATION_REQUEST_LIMIT) {
|
|
195
230
|
return;
|
|
@@ -223,6 +258,7 @@ function createHostedNativeSessionController(input) {
|
|
|
223
258
|
return current;
|
|
224
259
|
}
|
|
225
260
|
async function ensureAuthority(session, at) {
|
|
261
|
+
if (terminal) throw new HostedNativeSessionError(terminal.code);
|
|
226
262
|
if (session.authority && session.authority.clockEpoch === clock.epoch && session.authority.validUntil - at > REFRESH_MARGIN_MS) {
|
|
227
263
|
return { authority: session.authority, session };
|
|
228
264
|
}
|
|
@@ -230,20 +266,28 @@ function createHostedNativeSessionController(input) {
|
|
|
230
266
|
if (binding.kind !== "hosted") {
|
|
231
267
|
throw new HostedNativeSessionError("NATIVE_SESSION_REPLACED");
|
|
232
268
|
}
|
|
269
|
+
if (retryAt !== null && retryError && at < retryAt) {
|
|
270
|
+
if (session.authority && session.authority.clockEpoch === clock.epoch && session.authority.validUntil > at) {
|
|
271
|
+
return { authority: session.authority, session };
|
|
272
|
+
}
|
|
273
|
+
throw new HostedNativeSessionError(retryError.code);
|
|
274
|
+
}
|
|
233
275
|
const operationBinding = binding;
|
|
234
276
|
const abort = new SessionAbortController();
|
|
235
277
|
operations.add(abort);
|
|
236
278
|
const operation = {
|
|
237
279
|
abort,
|
|
238
280
|
promise: Promise.resolve().then(async () => {
|
|
239
|
-
session.error = null;
|
|
240
|
-
publishSession(session, "loading");
|
|
241
|
-
if (abort.signal.aborted) throw abort.error();
|
|
242
281
|
try {
|
|
282
|
+
if (abort.signal.aborted) throw abort.error();
|
|
283
|
+
session.error = null;
|
|
284
|
+
publishSession(session, "loading");
|
|
285
|
+
if (abort.signal.aborted) throw abort.error();
|
|
243
286
|
const result = await bootstrap(session, operationBinding, abort, true, true);
|
|
244
287
|
if (abort.signal.aborted) throw abort.error();
|
|
245
288
|
result.session.authority = result.authority;
|
|
246
289
|
result.session.error = null;
|
|
290
|
+
if (binding === operationBinding) clearRetry();
|
|
247
291
|
if (activeSession === result.session) {
|
|
248
292
|
publishSession(result.session, "ready");
|
|
249
293
|
if (abort.signal.aborted) throw abort.error();
|
|
@@ -252,6 +296,22 @@ function createHostedNativeSessionController(input) {
|
|
|
252
296
|
} catch (error) {
|
|
253
297
|
const normalized = abort.signal.aborted ? abort.error() : normalizeError(error);
|
|
254
298
|
const target = latestSession(session);
|
|
299
|
+
if (normalized.kind === "terminal" && binding === operationBinding && !terminal) {
|
|
300
|
+
const failure = Object.freeze({ code: normalized.code, kind: normalized.kind });
|
|
301
|
+
terminal = failure;
|
|
302
|
+
const rejectedSession = activeSession;
|
|
303
|
+
if (rejectedSession) {
|
|
304
|
+
rejectedSession.authority = null;
|
|
305
|
+
rejectedSession.error = failure;
|
|
306
|
+
}
|
|
307
|
+
for (const pending of [...operations]) pending.abort(failure.code);
|
|
308
|
+
if (binding === operationBinding && rejectedSession) {
|
|
309
|
+
publishSession(rejectedSession, "error", failure);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (normalized.kind === "recoverable" && binding === operationBinding && !terminal) {
|
|
313
|
+
delayRetry(normalized);
|
|
314
|
+
}
|
|
255
315
|
if (normalized.code !== "NATIVE_SESSION_REPLACED" && normalized.code !== "NATIVE_SESSION_DISPOSED") {
|
|
256
316
|
target.error = Object.freeze({ code: normalized.code, kind: normalized.kind });
|
|
257
317
|
publishSession(target, "error", target.error);
|
|
@@ -284,7 +344,10 @@ function createHostedNativeSessionController(input) {
|
|
|
284
344
|
try {
|
|
285
345
|
body = source.length ? JSON.parse(source) : null;
|
|
286
346
|
} catch {
|
|
287
|
-
|
|
347
|
+
if (response.status === 201) {
|
|
348
|
+
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_JSON_INVALID");
|
|
349
|
+
}
|
|
350
|
+
body = null;
|
|
288
351
|
}
|
|
289
352
|
if (response.status === 409 && allowRestart && isExactRestart(body, session.sessionId)) {
|
|
290
353
|
assertCurrentSession(session, hostedBinding);
|
|
@@ -292,7 +355,12 @@ function createHostedNativeSessionController(input) {
|
|
|
292
355
|
if (abort.signal.aborted) throw abort.error();
|
|
293
356
|
return bootstrap(replacement, hostedBinding, abort, false, false);
|
|
294
357
|
}
|
|
295
|
-
if (response.status !== 201)
|
|
358
|
+
if (response.status !== 201) {
|
|
359
|
+
const failure = statusError(response.status, body);
|
|
360
|
+
const delay = retryAfterMs(response.headers, now);
|
|
361
|
+
if (delay !== null) retryAfterDelays.set(failure, delay);
|
|
362
|
+
throw failure;
|
|
363
|
+
}
|
|
296
364
|
if (!hasNoStore(response.headers.get("cache-control"))) {
|
|
297
365
|
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
|
|
298
366
|
}
|
|
@@ -365,6 +433,16 @@ function createHostedNativeSessionController(input) {
|
|
|
365
433
|
if (binding.kind === "direct" || !activeSession) {
|
|
366
434
|
return Object.freeze({ status: "idle", source: "direct", lifecycle });
|
|
367
435
|
}
|
|
436
|
+
if (terminal) {
|
|
437
|
+
return Object.freeze({
|
|
438
|
+
status: "error",
|
|
439
|
+
source: "hosted",
|
|
440
|
+
lifecycle,
|
|
441
|
+
mapId: binding.mapId,
|
|
442
|
+
sessionId: activeSession.sessionId,
|
|
443
|
+
error: terminal
|
|
444
|
+
});
|
|
445
|
+
}
|
|
368
446
|
if (activeSession.operation) {
|
|
369
447
|
return Object.freeze({
|
|
370
448
|
status: "loading",
|
|
@@ -416,6 +494,7 @@ function createHostedNativeSessionController(input) {
|
|
|
416
494
|
if (bindingGeneration !== expectedGeneration) {
|
|
417
495
|
throw new HostedNativeSessionError("NATIVE_SESSION_REPLACED");
|
|
418
496
|
}
|
|
497
|
+
if (terminal) throw new HostedNativeSessionError(terminal.code);
|
|
419
498
|
checkedAt = readClock();
|
|
420
499
|
if (resolved.authority.clockEpoch === clock.epoch && resolved.authority.validUntil > checkedAt) {
|
|
421
500
|
admittedAuthorities.set(resolved.authority.public, resolved.authority);
|
|
@@ -470,7 +549,7 @@ function createHostedNativeSessionController(input) {
|
|
|
470
549
|
}
|
|
471
550
|
},
|
|
472
551
|
transportBudget(authority) {
|
|
473
|
-
if (disposed || lifecycle !== "foreground") return 0;
|
|
552
|
+
if (disposed || terminal || lifecycle !== "foreground") return 0;
|
|
474
553
|
const stored = admittedAuthorities.get(authority);
|
|
475
554
|
if (!stored || stored.bindingGeneration !== bindingGeneration) return 0;
|
|
476
555
|
try {
|
|
@@ -489,6 +568,11 @@ function createHostedNativeSessionController(input) {
|
|
|
489
568
|
async resume() {
|
|
490
569
|
if (disposed) throw new HostedNativeSessionError("NATIVE_SESSION_DISPOSED");
|
|
491
570
|
lifecycle = "foreground";
|
|
571
|
+
if (terminal) {
|
|
572
|
+
const error = new HostedNativeSessionError(terminal.code);
|
|
573
|
+
publish(currentSnapshot());
|
|
574
|
+
throw error;
|
|
575
|
+
}
|
|
492
576
|
if (binding.kind === "direct" || !activeSession) {
|
|
493
577
|
publish(currentSnapshot());
|
|
494
578
|
return;
|
|
@@ -511,9 +595,27 @@ function createHostedNativeSessionController(input) {
|
|
|
511
595
|
bindingGeneration += 1;
|
|
512
596
|
for (const operation of operations) operation.abort("NATIVE_SESSION_REPLACED");
|
|
513
597
|
binding = next;
|
|
598
|
+
terminal = null;
|
|
599
|
+
clearRetry();
|
|
514
600
|
activeSession = next.kind === "hosted" ? createSession(next.surfaceId, readClock()) : null;
|
|
515
601
|
publish(snapshotIdle(binding, activeSession, lifecycle));
|
|
516
602
|
},
|
|
603
|
+
retry() {
|
|
604
|
+
if (disposed) throw new HostedNativeSessionError("NATIVE_SESSION_DISPOSED");
|
|
605
|
+
if (!terminal && retryAt === null) return;
|
|
606
|
+
terminal = null;
|
|
607
|
+
clearRetry();
|
|
608
|
+
if (activeSession && !activeSession.operation) activeSession.error = null;
|
|
609
|
+
publish(currentSnapshot());
|
|
610
|
+
},
|
|
611
|
+
retryDelay() {
|
|
612
|
+
if (disposed || terminal || retryAt === null) return null;
|
|
613
|
+
try {
|
|
614
|
+
return Math.max(0, Math.ceil(retryAt - readClock()));
|
|
615
|
+
} catch {
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
},
|
|
517
619
|
subscribe(listener) {
|
|
518
620
|
if (typeof listener !== "function") {
|
|
519
621
|
throw new HostedNativeSessionError("NATIVE_SESSION_INPUT_INVALID");
|
|
@@ -608,7 +710,8 @@ function parseSuccess(value, binding, expectedSessionId, expectedSurfaceId) {
|
|
|
608
710
|
const issuedAtMs = parseExactIso(value.issuedAt);
|
|
609
711
|
const serverTimeMs = parseExactIso(value.serverTime);
|
|
610
712
|
const expiresAtMs = parseExactIso(value.expiresAt);
|
|
611
|
-
if (issuedAtMs > serverTimeMs + FUTURE_ISSUE_SKEW_MS || expiresAtMs <= serverTimeMs || expiresAtMs <= issuedAtMs || expiresAtMs - issuedAtMs > MAX_GRANT_LIFETIME_MS ||
|
|
713
|
+
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.
|
|
714
|
+
expiresAtMs - serverTimeMs > MAX_GRANT_LIFETIME_MS) {
|
|
612
715
|
throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
|
|
613
716
|
}
|
|
614
717
|
const authority = {
|
|
@@ -787,18 +890,46 @@ function decodeUtf8(bytes) {
|
|
|
787
890
|
function hasNoStore(value) {
|
|
788
891
|
return typeof value === "string" && value.split(",").some((part) => part.trim().toLowerCase() === "no-store");
|
|
789
892
|
}
|
|
790
|
-
function statusError(status) {
|
|
893
|
+
function statusError(status, body) {
|
|
791
894
|
if (status === 401) return new HostedNativeSessionError("NATIVE_SESSION_CREDENTIAL_REJECTED");
|
|
792
895
|
if (status === 403) return new HostedNativeSessionError("NATIVE_SESSION_MAP_MISMATCH");
|
|
793
896
|
if (status === 404) return new HostedNativeSessionError("NATIVE_SESSION_MAP_NOT_FOUND");
|
|
794
|
-
if (status === 429)
|
|
795
|
-
|
|
897
|
+
if (status === 429) {
|
|
898
|
+
return new HostedNativeSessionError(
|
|
899
|
+
isRecord(body) && typeof body.code === "string" && QUOTA_CODES.has(body.code) ? "NATIVE_SESSION_QUOTA_EXCEEDED" : "NATIVE_SESSION_RATE_LIMITED"
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
if (status === 408 || status >= 500 && status <= 599) {
|
|
903
|
+
return new HostedNativeSessionError("NATIVE_SESSION_UNAVAILABLE");
|
|
904
|
+
}
|
|
796
905
|
return new HostedNativeSessionError("NATIVE_SESSION_REJECTED");
|
|
797
906
|
}
|
|
907
|
+
var retryAfterDelays = /* @__PURE__ */ new WeakMap();
|
|
908
|
+
function retryAfterMs(headers, now) {
|
|
909
|
+
let value;
|
|
910
|
+
try {
|
|
911
|
+
value = headers.get("retry-after");
|
|
912
|
+
} catch {
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
if (typeof value !== "string" || value.length > 64) return null;
|
|
916
|
+
const text = value.trim();
|
|
917
|
+
let delay;
|
|
918
|
+
if (RETRY_AFTER_SECONDS.test(text)) delay = Number(text) * 1e3;
|
|
919
|
+
else {
|
|
920
|
+
const date = Date.parse(text);
|
|
921
|
+
if (!Number.isFinite(date)) return null;
|
|
922
|
+
try {
|
|
923
|
+
delay = date - readWallClock(now);
|
|
924
|
+
} catch {
|
|
925
|
+
return null;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return Math.min(Math.max(0, delay), RETRY_AFTER_LIMIT_MS);
|
|
929
|
+
}
|
|
798
930
|
function errorKind(code) {
|
|
799
931
|
if (code === "NATIVE_SESSION_DISPOSED" || code === "NATIVE_SESSION_REPLACED") return "cancelled";
|
|
800
|
-
|
|
801
|
-
return "terminal";
|
|
932
|
+
return TERMINAL_CODES.has(code) ? "terminal" : "recoverable";
|
|
802
933
|
}
|
|
803
934
|
function normalizeError(error, fallback = "NATIVE_SESSION_RESPONSE_INVALID") {
|
|
804
935
|
return error instanceof HostedNativeSessionError ? error : new HostedNativeSessionError(fallback);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TileflowNativeSourceState } from '@tileflow/core/native';
|
|
2
|
-
import {
|
|
2
|
+
import { x as MapSourceState } from '../contract-BaK24QZD.js';
|
|
3
3
|
import '@maplibre/maplibre-react-native';
|
|
4
4
|
import 'react';
|
|
5
5
|
import 'react-native';
|
package/docs/native-admission.md
CHANGED
|
@@ -59,6 +59,22 @@ Bootstrap uses an independent OkHttp client or ephemeral URLSession channel. It
|
|
|
59
59
|
|
|
60
60
|
Each context binds its validated bootstrap endpoint/credential. Retiring that context cancels its bootstrap without cancelling another Map. TypeScript alone parses session authority and makes admission decisions.
|
|
61
61
|
|
|
62
|
+
A bootstrap ends after 10 seconds, well inside the 30-second protected transport deadline. A stalled renewal therefore fails its bridge batch, and the tickets waiting on it, instead of outliving them. A ticket whose own deadline passes while JavaScript is still answering its batch fails alone; only a batch that JavaScript does not answer within 30 seconds retires the context. When admission retires the context of a mounted Map, the Map reports `renderer-error` and opens a new context for the same source, up to three times without a successful load in between.
|
|
63
|
+
|
|
64
|
+
## Session failures
|
|
65
|
+
|
|
66
|
+
The controller classifies each bootstrap or renewal failure:
|
|
67
|
+
|
|
68
|
+
| Response | Result |
|
|
69
|
+
| ------------------------------------------------------------------------------ | ------------------------------------------------------- |
|
|
70
|
+
| 401, 403, 404 | Stops the session until `ref.retry()` or another source |
|
|
71
|
+
| 429 with `COMMERCIAL_MAP_LIMIT_REACHED` or `COMMERCIAL_SPENDING_LIMIT_REACHED` | Stops the session until `ref.retry()` or another source |
|
|
72
|
+
| Any other 429 | Rate limited: retries after a delay |
|
|
73
|
+
| 408, 5xx, network failure or timeout | Unavailable: retries after a delay |
|
|
74
|
+
| Another status, or a response that breaks the session contract | Retries after a delay |
|
|
75
|
+
|
|
76
|
+
A stopped session reports `session-error` through `onError`, including when a renewal is refused after the first load. A delay starts at one second and doubles up to one minute; a `Retry-After` in seconds or as an HTTP date lengthens it, up to 15 minutes. During the delay no bootstrap leaves the device, and a grant that is still valid keeps admitting protected requests. A successful bootstrap or `ref.retry()` clears the delay.
|
|
77
|
+
|
|
62
78
|
The controller's private transport budget remains anchored to its existing local `validUntil`. Native never reconstructs a fresh lifetime from server timestamps. The budget is bounded by the earlier ticket-enqueue time, the native wait deadline and a safety margin, and is rechecked immediately before network start and redirects. Backgrounding, clock rollback, source replacement, disposal and expiry invalidate stale authority.
|
|
63
79
|
|
|
64
80
|
No synchronous network hook, main thread or OkHttp interceptor waits for JavaScript. Suspended JavaScript therefore cannot authorize stale protected work.
|
|
@@ -93,7 +109,7 @@ Camera commands carry private monotonically correlated tokens. A successful publ
|
|
|
93
109
|
| Protected response body | 8,388,608 bytes |
|
|
94
110
|
| Pending bootstrap bridge work | 32 |
|
|
95
111
|
| Bootstrap request / response | 2,048 / 65,536 bytes |
|
|
96
|
-
| Bootstrap deadline |
|
|
112
|
+
| Bootstrap deadline | 10 seconds |
|
|
97
113
|
|
|
98
114
|
Cancellation retains transport capacity until the corresponding native cleanup boundary. Capacity bounds are transport safety limits, not product quotas or billing counters.
|
|
99
115
|
|
|
@@ -102,11 +102,13 @@ static NSTextCheckingResult *TFBootstrapMatch(NSString *value, NSString *pattern
|
|
|
102
102
|
context.endpoint = clean; context.credential = credential; context.lastRequest = sequence;
|
|
103
103
|
TFBootstrapWork *work = [TFBootstrapWork new];
|
|
104
104
|
work.identifier = requestId; work.context = context; work.alive = [TFBootstrapFlag new];
|
|
105
|
-
|
|
105
|
+
// Well inside the 30 s ticket and batch deadlines, so a stalled renewal fails its batch
|
|
106
|
+
// instead of letting the context retire under it.
|
|
107
|
+
work.deadline = [self.scheduler nowMs] + 10000; work.completion = completion;
|
|
106
108
|
context.pending[requestId] = work; self.pendingCount++;
|
|
107
|
-
work.timer = [self.scheduler after:
|
|
109
|
+
work.timer = [self.scheduler after:10000 perform:^{ [self finish:work reply:nil]; }];
|
|
108
110
|
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:clean]
|
|
109
|
-
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:
|
|
111
|
+
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:10];
|
|
110
112
|
request.HTTPMethod = @"POST"; request.HTTPBody = [body dataUsingEncoding:NSUTF8StringEncoding];
|
|
111
113
|
request.HTTPShouldHandleCookies = NO;
|
|
112
114
|
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
|
|
@@ -115,13 +117,17 @@ static NSTextCheckingResult *TFBootstrapMatch(NSString *value, NSString *pattern
|
|
|
115
117
|
@try {
|
|
116
118
|
work.network = [self.network start:request mayStart:^BOOL { return [self mayStart:work]; } completion:^(NSHTTPURLResponse *response, NSData *bytes) {
|
|
117
119
|
[self.scheduler enqueue:^{
|
|
118
|
-
NSString *cacheControl = @"";
|
|
120
|
+
NSString *cacheControl = @"", *retryAfter = @"";
|
|
119
121
|
for (id name in response.allHeaderFields) {
|
|
120
|
-
if ([name isKindOfClass:NSString.class]
|
|
122
|
+
if (![name isKindOfClass:NSString.class]) continue;
|
|
123
|
+
if ([(NSString *)name caseInsensitiveCompare:@"Cache-Control"] == NSOrderedSame) cacheControl = response.allHeaderFields[name];
|
|
124
|
+
if ([(NSString *)name caseInsensitiveCompare:@"Retry-After"] == NSOrderedSame) retryAfter = response.allHeaderFields[name];
|
|
121
125
|
}
|
|
126
|
+
// An oversized Retry-After is ignored rather than failing the reply.
|
|
127
|
+
if (!TFBootstrapString(retryAfter, 64)) retryAfter = @"";
|
|
122
128
|
BOOL valid = [self mayStart:work] && [response isKindOfClass:NSHTTPURLResponse.class] && response.statusCode >= 100 && response.statusCode <= 599 &&
|
|
123
129
|
TFBootstrapString(cacheControl, 1024) && [bytes isKindOfClass:NSData.class] && bytes.length <= 65536;
|
|
124
|
-
NSDictionary *reply = valid ? @{@"status": @(response.statusCode), @"cacheControl": cacheControl, @"bodyBase64": [bytes base64EncodedStringWithOptions:0]} : nil;
|
|
130
|
+
NSDictionary *reply = valid ? @{@"status": @(response.statusCode), @"cacheControl": cacheControl, @"retryAfter": retryAfter, @"bodyBase64": [bytes base64EncodedStringWithOptions:0]} : nil;
|
|
125
131
|
[self finish:work reply:reply];
|
|
126
132
|
}];
|
|
127
133
|
}];
|
package/ios/TFAdmissionEngine.mm
CHANGED
|
@@ -168,8 +168,10 @@ static BOOL TFGrantShape(NSString *value) {
|
|
|
168
168
|
work.response = response; work.failure = failure; work.delegate = delegate; work.releaseReservation = release;
|
|
169
169
|
context.work[work.identifier] = work; [context.order addObject:work.identifier];
|
|
170
170
|
work.timer = [self.scheduler after:30000 perform:^{
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
// A slow renewal fails only this request. JavaScript still answers its batch, and the
|
|
172
|
+
// batch deadline alone retires a context whose JavaScript stopped answering.
|
|
173
|
+
if ([context.batchTickets containsObject:work.identifier]) [self event:context kind:@"cancel" fields:@{@"tickets": @[work.identifier]}];
|
|
174
|
+
[self fail:work];
|
|
173
175
|
}];
|
|
174
176
|
[self.scheduler enqueue:^{ [self drain:context]; }];
|
|
175
177
|
}];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tileflow/react-native",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.7",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Tileflow maps, camera control and semantic interactions for React Native.",
|
|
6
6
|
"type": "module",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@tileflow/core": ">=0.1.0-alpha.
|
|
39
|
+
"@tileflow/core": ">=0.1.0-alpha.42 <0.1.0-beta.0",
|
|
40
40
|
"@tileflow/interactions": ">=0.1.0-alpha.6 <0.1.0-beta.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|