tangled
alpha
login
or
join now
finxol.io
/
hooks
0
fork
atom
Simple API gateway for webhooks
0
fork
atom
overview
issues
pulls
pipelines
feat: cache data whenever possible
finxol.io
6 months ago
080be79a
641b4660
verified
This commit was signed with the committer's
known signature
.
finxol.io
SSH Key Fingerprint:
SHA256:olFE3asYdoBMScuJOt60UxXdJ0RFdGv5kVKrdOtIcPI=
1/1
deploy.yaml
success
22s
+99
-12
4 changed files
expand all
collapse all
unified
split
deno.lock
src
main.ts
sensors.ts
util.ts
+10
deno.lock
···
1
1
{
2
2
"version": "5",
3
3
"specifiers": {
4
4
+
"jsr:@std/crypto@*": "1.0.5",
5
5
+
"jsr:@std/encoding@*": "1.0.10",
4
6
"npm:@hono/ua-blocker@~0.1.9": "0.1.9_hono@4.9.6",
5
7
"npm:@types/node@*": "24.2.0",
6
8
"npm:hono@^4.9.6": "4.9.6",
7
9
"npm:zod@^4.1.5": "4.1.5"
10
10
+
},
11
11
+
"jsr": {
12
12
+
"@std/crypto@1.0.5": {
13
13
+
"integrity": "0dcfbb319fe0bba1bd3af904ceb4f948cde1b92979ec1614528380ed308a3b40"
14
14
+
},
15
15
+
"@std/encoding@1.0.10": {
16
16
+
"integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1"
17
17
+
}
8
18
},
9
19
"npm": {
10
20
"@hono/ua-blocker@0.1.9_hono@4.9.6": {
+1
-1
src/main.ts
···
3
3
import { aiBots, useAiRobotsTxt } from "@hono/ua-blocker/ai-bots"
4
4
import { config } from "../config.ts"
5
5
import { sensors } from "./sensors.ts"
6
6
+
import { kv } from "./util.ts"
6
7
7
8
const app = new Hono()
8
9
.route("/sensors", sensors)
···
53
54
})
54
55
55
56
if (Deno.env.get("DENO_ENV") === "dev") {
56
56
-
const kv = await Deno.openKv()
57
57
const { default: data } = await import("../test.json", { with: { type: "json" } })
58
58
kv.set(
59
59
["sensors", "latest"],
+26
-11
src/sensors.ts
···
1
1
import { Hono } from "hono"
2
2
import { validator } from "hono/validator"
3
3
import { z } from "zod"
4
4
-
import { tryCatch } from "./util.ts"
4
4
+
import { fetchWithCache, kv } from "./util.ts"
5
5
6
6
const SensorsSchema = z.object({
7
7
sensorData: z.array(
···
128
128
129
129
type Sensors = z.infer<typeof SensorsSchema>
130
130
131
131
-
const kv = await Deno.openKv()
131
131
+
type Country = {
132
132
+
country: string
133
133
+
country_code: string
134
134
+
}
132
135
133
136
const sensors = new Hono()
134
137
.get("/country", async (c) => {
138
138
+
// Served cached data if available
139
139
+
const cached = await kv.get<Country>(["country"])
140
140
+
if (cached.value) {
141
141
+
console.info("Serving cached country data")
142
142
+
return c.json(cached.value)
143
143
+
}
144
144
+
145
145
+
// Fetch sensors data from KV
135
146
const data = await kv.get<Sensors>(["sensors", "latest"])
136
147
if (!data.value) {
137
148
return c.text("No data found", 404)
138
149
}
139
150
151
151
+
// Extract location from sensors data
140
152
const location = data.value.sensorData.find((sensor) =>
141
153
sensor.sensorType === "location"
142
154
)?.data
···
145
157
return c.text("No location data found", 404)
146
158
}
147
159
148
148
-
const geocode = await tryCatch(
149
149
-
fetch(
150
150
-
`https://api.geoapify.com/v1/geocode/reverse?lat=${location.latitude}&lon=${location.longitude}&apiKey=${
151
151
-
Deno.env.get("GEOAPIFY_API_KEY")
152
152
-
}`,
153
153
-
)
154
154
-
.then((res) => res.json()),
160
160
+
// Extract geocode from location data
161
161
+
const geocode = await fetchWithCache<Country>(
162
162
+
`https://api.geoapify.com/v1/geocode/reverse?lat=${location.latitude}&lon=${location.longitude}&apiKey=${
163
163
+
Deno.env.get("GEOAPIFY_API_KEY")
164
164
+
}`,
155
165
)
156
166
157
167
if (!geocode.success) {
···
166
176
return c.text("Invalid country data", 400)
167
177
}
168
178
169
169
-
return c.json({
179
179
+
const ret = {
170
180
country: country.data.features[0].properties.country,
171
181
country_code: country.data.features[0].properties.country_code,
172
172
-
})
182
182
+
}
183
183
+
184
184
+
// Cache country data to KV store (expiry set for 2 hours)
185
185
+
await kv.set(["country"], ret satisfies Country, { expireIn: 60 * 60 * 2 })
186
186
+
187
187
+
return c.json(ret)
173
188
})
174
189
.get("/get", async (c) => {
175
190
const data = await kv.get<Sensors>(["sensors", "latest"])
+62
src/util.ts
···
1
1
+
import { crypto } from "jsr:@std/crypto"
2
2
+
import { encodeHex } from "jsr:@std/encoding/hex"
3
3
+
4
4
+
export const kv = await Deno.openKv()
5
5
+
1
6
/**
2
7
* Wraps a promise in a try/catch block and returns a Result object representing
3
8
* either a successful value or an error.
···
55
60
return { success: false, value: null, error: error as E }
56
61
}
57
62
}
63
63
+
64
64
+
/**
65
65
+
* Fetches data from a URL with caching.
66
66
+
* Cache is stored in the KV
67
67
+
* @param url - The URL to fetch data from.
68
68
+
* @returns A promise that resolves to an object with success status, value, and error.
69
69
+
*/
70
70
+
export async function fetchWithCache<T>(url: string): Promise<
71
71
+
| {
72
72
+
success: true
73
73
+
value: T
74
74
+
error: null
75
75
+
}
76
76
+
| {
77
77
+
success: false
78
78
+
value: null
79
79
+
error: string
80
80
+
}
81
81
+
> {
82
82
+
// Calculate the hash of the request URL
83
83
+
const keybuffer = new TextEncoder().encode(url)
84
84
+
const hashBuffer = await crypto.subtle.digest("SHA-256", keybuffer)
85
85
+
const hash = encodeHex(hashBuffer)
86
86
+
87
87
+
// Check if the data is already in cache
88
88
+
const cached = await kv.get<T>(["fetch-cache", hash])
89
89
+
if (cached.value) {
90
90
+
console.info("Serving cached request data")
91
91
+
return {
92
92
+
success: true,
93
93
+
value: cached.value,
94
94
+
error: null,
95
95
+
}
96
96
+
}
97
97
+
98
98
+
// Fetch the data from the URL
99
99
+
const data = await tryCatch(
100
100
+
fetch(url).then((res) => res.json() as T),
101
101
+
)
102
102
+
103
103
+
if (!data.success) {
104
104
+
return {
105
105
+
success: false,
106
106
+
value: null,
107
107
+
error: data.error.message,
108
108
+
}
109
109
+
}
110
110
+
111
111
+
// Store the fetched data in cache, with a 1-day expiration
112
112
+
await kv.set(["fetch-cache", hash], data.value, { expireIn: 60 * 60 * 24 })
113
113
+
114
114
+
return {
115
115
+
success: true,
116
116
+
value: data.value,
117
117
+
error: null,
118
118
+
}
119
119
+
}