• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

OneBusAway / wayfinder / 23038129192

13 Mar 2026 05:50AM UTC coverage: 80.047%. First build
23038129192

Pull #384

github

web-flow
Merge 3e55693e7 into 2f8481428
Pull Request #384: Release 2026.4

1751 of 1940 branches covered (90.26%)

Branch coverage included in aggregate %.

794 of 836 new or added lines in 24 files covered. (94.98%)

11235 of 14283 relevant lines covered (78.66%)

4.25 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.42
/src/lib/otpServerCache.js
1
import { PUBLIC_OTP_SERVER_URL } from '$env/static/public';
1✔
2

3
/** @type {'graphql' | 'rest' | null} */
1✔
4
let otpApiType = null;
1✔
5

6
/** @type {number | null} */
1✔
7
let cacheTimestamp = null;
1✔
8

9
/** @type {Promise<void> | null} */
1✔
10
let initPromise = null;
1✔
11

12
// Cache TTL: 1 hour
1✔
13
const CACHE_TTL = 3600000;
1✔
14

15
// Maximum time to wait for the OTP server to respond before giving up
1✔
16
const DETECT_TIMEOUT = 10_000;
1✔
17

18
// After a cold-start failure, minimum gap before retrying
1✔
19
const ERROR_RETRY_DELAY = 30_000;
1✔
20

21
/** @type {number | null} */
1✔
22
let lastErrorTime = null;
1✔
23

24
async function detectOtpVersion() {
19✔
25
        const ac = new AbortController();
19✔
26
        const timer = setTimeout(() => ac.abort(), DETECT_TIMEOUT);
19✔
27
        try {
19✔
28
                const response = await fetch(PUBLIC_OTP_SERVER_URL, { signal: ac.signal });
19✔
29

30
                if (!response.ok) {
19✔
31
                        throw new Error(`OTP server returned HTTP ${response.status}`);
2✔
32
                }
2✔
33

34
                const contentType = response.headers.get('content-type') || '';
19✔
35

36
                if (contentType.includes('application/json')) {
19✔
37
                        const data = await response.json();
11✔
38
                        return data.version?.major >= 2 ? 'graphql' : 'rest';
11✔
39
                }
11✔
40

41
                // OTP 1.x returns XML — treat as REST
3✔
42
                return 'rest';
3✔
43
        } finally {
19✔
44
                clearTimeout(timer);
18✔
45
        }
18✔
46
}
19✔
47

48
/**
1✔
49
 * Preloads OTP version detection into cache with TTL support.
1✔
50
 * Skips detection if PUBLIC_OTP_SERVER_URL is not configured.
1✔
51
 *
1✔
52
 * Stale-while-revalidate: when a cached type exists but is past TTL, a
1✔
53
 * background refresh is kicked off and the caller returns immediately so the
1✔
54
 * handle hook is not blocked. On a cold start (no type detected yet) the
1✔
55
 * caller waits up to DETECT_TIMEOUT milliseconds (bounded by the AbortController
1✔
56
 * inside detectOtpVersion) before proceeding.
1✔
57
 *
1✔
58
 * @param {boolean} [forceRefresh=false]
1✔
59
 * @returns {Promise<void>}
1✔
60
 */
1✔
61
export async function preloadOtpVersion(forceRefresh = false) {
1✔
62
        if (!PUBLIC_OTP_SERVER_URL) {
26✔
63
                return;
1✔
64
        }
1✔
65

66
        const now = Date.now();
25✔
67
        const isStale = cacheTimestamp !== null && now - cacheTimestamp > CACHE_TTL;
26✔
68

69
        if (otpApiType && !isStale && !forceRefresh) {
26✔
70
                return;
2✔
71
        }
2✔
72

73
        // Error cooldown: after a failure, don't immediately retry when there's no cached value.
23✔
74
        if (!otpApiType && lastErrorTime !== null && now - lastErrorTime < ERROR_RETRY_DELAY) {
26✔
75
                console.debug('[otpServerCache] Within error cooldown — serving without cached OTP version');
2✔
76
                return;
2✔
77
        }
2✔
78

79
        // A refresh is already in flight
21✔
80
        if (initPromise) {
26✔
81
                // Stale-while-revalidate: we have a value, don't block on background refresh
2✔
82
                if (otpApiType && !forceRefresh) {
2!
NEW
83
                        return;
×
NEW
84
                }
×
85
                await initPromise;
2✔
86
                return;
2✔
87
        }
2✔
88

89
        const promise = detectOtpVersion()
19✔
90
                .then((type) => {
19✔
91
                        otpApiType = type;
14✔
92
                        cacheTimestamp = Date.now();
14✔
93
                        lastErrorTime = null;
14✔
94
                })
19✔
95
                .catch((err) => {
19✔
96
                        if (err.name === 'AbortError') {
4✔
97
                                console.warn(`[otpServerCache] OTP version detection timed out after ${DETECT_TIMEOUT}ms`);
1✔
98
                        } else {
4✔
99
                                console.error('[otpServerCache] OTP version detection failed:', err.message);
3✔
100
                        }
3✔
101
                        lastErrorTime = Date.now();
4✔
102
                        if (otpApiType) {
4!
NEW
103
                                cacheTimestamp = Date.now();
×
NEW
104
                        }
×
105
                })
19✔
106
                .finally(() => {
19✔
107
                        initPromise = null;
18✔
108
                });
19✔
109

110
        initPromise = promise;
19✔
111

112
        // Stale-while-revalidate: background refresh started, return immediately
19✔
113
        if (otpApiType && !forceRefresh) {
26✔
114
                return;
2✔
115
        }
2✔
116

117
        // Cold start: wait for detection (bounded by the AbortController in detectOtpVersion)
17✔
118
        await promise;
17✔
119
}
17✔
120

121
/**
1✔
122
 * Gets the detected OTP API type.
1✔
123
 * @returns {'graphql' | 'rest' | null}
1✔
124
 */
1✔
125
export function getOtpApiType() {
1✔
126
        return otpApiType;
22✔
127
}
22✔
128

129
/**
1✔
130
 * Clears cached OTP version data (for testing).
1✔
131
 */
1✔
132
export function clearOtpCache() {
1✔
133
        otpApiType = null;
17✔
134
        cacheTimestamp = null;
17✔
135
        initPromise = null;
17✔
136
        lastErrorTime = null;
17✔
137
}
17✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc