-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathworker.js
More file actions
154 lines (135 loc) · 4.91 KB
/
worker.js
File metadata and controls
154 lines (135 loc) · 4.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
// Cloudflare Worker: serves static assets and proxies AirLabs API requests
// with lazy KV caching to reduce API calls
export default {
async fetch(request, env) {
const url = new URL(request.url);
// Proxy AirLabs API requests
if (url.pathname === '/api/schedules') {
return handleAirLabsProxy(url, env);
}
// For all other requests, pass through to static assets
return env.ASSETS.fetch(request);
}
};
// Default cache TTL in seconds (2 hours — matches free plan safe limit)
const DEFAULT_TTL = 7200;
async function handleAirLabsProxy(url, env) {
const apiKey = env.AIRLABS_API_KEY;
const backupKey = env.AIRLABS_API_KEY_BACKUP;
if (!apiKey) {
return new Response(JSON.stringify({ error: 'API key not configured' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
// Parse request params
const params = new URLSearchParams(url.search);
const depIata = params.get('dep_iata');
const arrIata = params.get('arr_iata');
const ttl = Math.max(60, parseInt(params.get('ttl'), 10) || DEFAULT_TTL);
// Build cache key: flights:<IATA>:arrival or flights:<IATA>:departure
const airportCode = arrIata || depIata || 'unknown';
const direction = arrIata ? 'arrival' : 'departure';
const cacheKey = `flights:${airportCode}:${direction}`;
// ── Try KV cache first ──
const kv = env.FLIGHT_CACHE;
if (kv) {
try {
const { value, metadata } = await kv.getWithMetadata(cacheKey, { type: 'text' });
if (value && metadata && metadata.fetchedAt) {
const age = (Date.now() - metadata.fetchedAt) / 1000;
if (age < ttl) {
// Cache hit — return cached response
return new Response(value, {
status: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Cache-Control': `public, max-age=${Math.max(1, Math.floor(ttl - age))}`,
'X-Cache': 'HIT',
'X-Cache-Age': String(Math.floor(age)),
},
});
}
}
} catch (_) {
// KV unavailable — fall through to live fetch
}
}
// ── Cache miss or stale — fetch from AirLabs ──
// Remove ttl param before forwarding to AirLabs
params.delete('ttl');
// Try primary key first, fallback to backup on rate limit / auth error
const keys = backupKey ? [apiKey, backupKey] : [apiKey];
let lastResp = null;
let lastData = null;
for (const key of keys) {
params.set('api_key', key);
const apiUrl = `https://airlabs.co/api/v9/schedules?${params.toString()}`;
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 15000);
const resp = await fetch(apiUrl, { signal: controller.signal });
clearTimeout(timeout);
const data = await resp.text();
lastResp = resp;
lastData = data;
// If rate limited (429) or auth error (401/403), try next key
if ((resp.status === 429 || resp.status === 401 || resp.status === 403) && key !== keys[keys.length - 1]) {
continue;
}
// Success or final key — write to cache and return
if (resp.status === 200 && kv) {
try {
kv.put(cacheKey, data, {
expirationTtl: ttl,
metadata: { fetchedAt: Date.now() },
});
} catch (_) {
// KV write failed — silent
}
}
return new Response(data, {
status: resp.status,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Cache-Control': resp.status === 200 ? `public, max-age=${ttl}` : 'no-cache',
'X-Cache': 'MISS',
},
});
} catch (err) {
// If this key timed out but there's a backup, try it
if (key !== keys[keys.length - 1]) continue;
// All keys exhausted — try serving stale cache
if (kv) {
try {
const stale = await kv.get(cacheKey, { type: 'text' });
if (stale) {
return new Response(stale, {
status: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'public, max-age=60',
'X-Cache': 'STALE',
},
});
}
} catch (_) {
// KV unavailable
}
}
const msg = err.name === 'AbortError' ? 'AirLabs API timed out' : 'Upstream request failed';
return new Response(JSON.stringify({ error: msg }), {
status: 504,
headers: { 'Content-Type': 'application/json' },
});
}
}
// Should not reach here, but handle gracefully
return new Response(lastData || JSON.stringify({ error: 'No API keys available' }), {
status: lastResp ? lastResp.status : 500,
headers: { 'Content-Type': 'application/json' },
});
}