@vltpkg/vsr 0.2.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/.editorconfig +13 -0
- package/.prettierrc +7 -0
- package/CONTRIBUTING.md +228 -0
- package/LICENSE.md +110 -0
- package/README.md +373 -0
- package/bin/vsr.ts +29 -0
- package/config.ts +124 -0
- package/debug-npm.js +19 -0
- package/drizzle.config.js +33 -0
- package/package.json +80 -0
- package/pnpm-workspace.yaml +5 -0
- package/src/api.ts +2246 -0
- package/src/assets/public/images/bg.png +0 -0
- package/src/assets/public/images/clients/logo-bun.png +0 -0
- package/src/assets/public/images/clients/logo-deno.png +0 -0
- package/src/assets/public/images/clients/logo-npm.png +0 -0
- package/src/assets/public/images/clients/logo-pnpm.png +0 -0
- package/src/assets/public/images/clients/logo-vlt.png +0 -0
- package/src/assets/public/images/clients/logo-yarn.png +0 -0
- package/src/assets/public/images/favicon/apple-touch-icon.png +0 -0
- package/src/assets/public/images/favicon/favicon-96x96.png +0 -0
- package/src/assets/public/images/favicon/favicon.ico +0 -0
- package/src/assets/public/images/favicon/favicon.svg +3 -0
- package/src/assets/public/images/favicon/site.webmanifest +21 -0
- package/src/assets/public/images/favicon/web-app-manifest-192x192.png +0 -0
- package/src/assets/public/images/favicon/web-app-manifest-512x512.png +0 -0
- package/src/assets/public/styles/styles.css +219 -0
- package/src/db/client.ts +544 -0
- package/src/db/migrations/0000_faulty_ricochet.sql +14 -0
- package/src/db/migrations/0000_initial.sql +29 -0
- package/src/db/migrations/0001_uuid_validation.sql +35 -0
- package/src/db/migrations/0001_wealthy_magdalene.sql +7 -0
- package/src/db/migrations/drop.sql +3 -0
- package/src/db/migrations/meta/0000_snapshot.json +104 -0
- package/src/db/migrations/meta/0001_snapshot.json +155 -0
- package/src/db/migrations/meta/_journal.json +20 -0
- package/src/db/schema.ts +41 -0
- package/src/index.ts +709 -0
- package/src/routes/access.ts +263 -0
- package/src/routes/auth.ts +93 -0
- package/src/routes/index.ts +135 -0
- package/src/routes/packages.ts +924 -0
- package/src/routes/search.ts +50 -0
- package/src/routes/static.ts +53 -0
- package/src/routes/tokens.ts +102 -0
- package/src/routes/users.ts +14 -0
- package/src/utils/auth.ts +145 -0
- package/src/utils/cache.ts +466 -0
- package/src/utils/database.ts +44 -0
- package/src/utils/packages.ts +337 -0
- package/src/utils/response.ts +100 -0
- package/src/utils/routes.ts +47 -0
- package/src/utils/spa.ts +14 -0
- package/src/utils/tracing.ts +63 -0
- package/src/utils/upstream.ts +131 -0
- package/test/README.md +91 -0
- package/test/access.test.js +760 -0
- package/test/cloudflare-waituntil.test.js +141 -0
- package/test/db.test.js +447 -0
- package/test/dist-tag.test.js +415 -0
- package/test/e2e.test.js +904 -0
- package/test/hono-context.test.js +250 -0
- package/test/integrity-validation.test.js +183 -0
- package/test/json-response.test.js +76 -0
- package/test/manifest-slimming.test.js +449 -0
- package/test/packument-consistency.test.js +351 -0
- package/test/packument-version-range.test.js +144 -0
- package/test/performance.test.js +162 -0
- package/test/route-with-waituntil.test.js +298 -0
- package/test/run-tests.js +151 -0
- package/test/setup-cache-tests.js +190 -0
- package/test/setup.js +64 -0
- package/test/stale-while-revalidate.test.js +273 -0
- package/test/static-assets.test.js +85 -0
- package/test/upstream-routing.test.js +86 -0
- package/test/utils/test-helpers.js +84 -0
- package/test/waituntil-correct.test.js +208 -0
- package/test/waituntil-demo.test.js +138 -0
- package/test/waituntil-readme.md +113 -0
- package/tsconfig.json +37 -0
- package/types.ts +446 -0
- package/vitest.config.js +95 -0
- package/wrangler.json +58 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache utilities for efficient upstream package management
|
|
3
|
+
*
|
|
4
|
+
* Strategy:
|
|
5
|
+
* - Return cached data immediately when available (even if stale)
|
|
6
|
+
* - Queue background refresh if cache is stale
|
|
7
|
+
* - Only fetch upstream synchronously if no cache exists
|
|
8
|
+
* - Use Cloudflare Queues for reliable background processing
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
HonoContext,
|
|
13
|
+
CacheOptions,
|
|
14
|
+
CacheResult,
|
|
15
|
+
CacheValidation,
|
|
16
|
+
QueueMessage,
|
|
17
|
+
ParsedPackage,
|
|
18
|
+
ParsedVersion,
|
|
19
|
+
PackageManifest
|
|
20
|
+
} from '../../types.ts'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Stale-while-revalidate cache strategy for package data
|
|
24
|
+
* Returns stale cache immediately and refreshes in background via queue
|
|
25
|
+
*/
|
|
26
|
+
export async function getCachedPackageWithRefresh<T>(
|
|
27
|
+
c: HonoContext,
|
|
28
|
+
packageName: string,
|
|
29
|
+
fetchUpstreamFn: () => Promise<T>,
|
|
30
|
+
options: CacheOptions = {}
|
|
31
|
+
): Promise<CacheResult<T>> {
|
|
32
|
+
const {
|
|
33
|
+
packumentTtlMinutes = 5, // Short TTL for packuments (they change frequently)
|
|
34
|
+
staleWhileRevalidateMinutes = 60, // Allow stale data for up to 1 hour while refreshing
|
|
35
|
+
forceRefresh = false,
|
|
36
|
+
upstream = 'npm'
|
|
37
|
+
} = options
|
|
38
|
+
|
|
39
|
+
// If forcing refresh, skip cache entirely
|
|
40
|
+
if (forceRefresh) {
|
|
41
|
+
console.log(`[CACHE] Force refresh requested for: ${packageName}`)
|
|
42
|
+
const upstreamData = await fetchUpstreamFn()
|
|
43
|
+
if (upstreamData) {
|
|
44
|
+
c.waitUntil?.(cachePackageData(c, packageName, upstreamData, options))
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
package: upstreamData,
|
|
48
|
+
fromCache: false,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// Get cached data first
|
|
54
|
+
const cachedResult = await getCachedPackageData(c, packageName, packumentTtlMinutes, staleWhileRevalidateMinutes)
|
|
55
|
+
|
|
56
|
+
if (cachedResult.data) {
|
|
57
|
+
const { valid, stale, data } = cachedResult
|
|
58
|
+
|
|
59
|
+
if (valid) {
|
|
60
|
+
// Cache is fresh - return immediately
|
|
61
|
+
console.log(`[CACHE] Using fresh cached package: ${packageName}`)
|
|
62
|
+
return {
|
|
63
|
+
package: data as T,
|
|
64
|
+
fromCache: true,
|
|
65
|
+
stale: false
|
|
66
|
+
}
|
|
67
|
+
} else if (stale) {
|
|
68
|
+
// Cache is stale but within stale-while-revalidate window
|
|
69
|
+
// Return stale data immediately and queue background refresh
|
|
70
|
+
console.log(`[CACHE] Using stale data for ${packageName}, queuing background refresh`)
|
|
71
|
+
|
|
72
|
+
// Queue background refresh using Cloudflare Queues
|
|
73
|
+
if (c.env.CACHE_REFRESH_QUEUE) {
|
|
74
|
+
await queuePackageRefresh(c, packageName, upstream, fetchUpstreamFn, options)
|
|
75
|
+
} else {
|
|
76
|
+
// Fallback to waitUntil if queue not available
|
|
77
|
+
console.log(`[CACHE] Queue not available, using waitUntil fallback for ${packageName}`)
|
|
78
|
+
c.waitUntil?.(refreshPackageInBackground(c, packageName, fetchUpstreamFn, options))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
package: data as T,
|
|
83
|
+
fromCache: true,
|
|
84
|
+
stale: true
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// No cache data available - fetch upstream synchronously
|
|
90
|
+
console.log(`[CACHE] No cache available for ${packageName}, fetching upstream`)
|
|
91
|
+
const upstreamData = await fetchUpstreamFn()
|
|
92
|
+
|
|
93
|
+
// Cache the fresh data in background
|
|
94
|
+
c.waitUntil?.(cachePackageData(c, packageName, upstreamData, options))
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
package: upstreamData,
|
|
98
|
+
fromCache: false,
|
|
99
|
+
stale: false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error(`[CACHE ERROR] Failed to get cached package ${packageName}: ${(error as Error).message}`)
|
|
104
|
+
throw error
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Queue a package refresh job using Cloudflare Queues
|
|
110
|
+
* Note: We can't serialize functions, so we'll need to recreate the fetch function in the queue consumer
|
|
111
|
+
*/
|
|
112
|
+
async function queuePackageRefresh<T>(
|
|
113
|
+
c: HonoContext,
|
|
114
|
+
packageName: string,
|
|
115
|
+
upstream: string,
|
|
116
|
+
fetchUpstreamFn: () => Promise<T>,
|
|
117
|
+
options: CacheOptions
|
|
118
|
+
): Promise<void> {
|
|
119
|
+
try {
|
|
120
|
+
const message: QueueMessage = {
|
|
121
|
+
type: 'package_refresh',
|
|
122
|
+
packageName,
|
|
123
|
+
upstream,
|
|
124
|
+
timestamp: Date.now(),
|
|
125
|
+
options: {
|
|
126
|
+
packumentTtlMinutes: options.packumentTtlMinutes || 5,
|
|
127
|
+
upstream: options.upstream || 'npm'
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await c.env.CACHE_REFRESH_QUEUE.send(message)
|
|
132
|
+
console.log(`[QUEUE] Queued refresh for package: ${packageName}`)
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error(`[QUEUE ERROR] Failed to queue refresh for ${packageName}: ${(error as Error).message}`)
|
|
135
|
+
// Fallback to immediate background refresh
|
|
136
|
+
c.waitUntil?.(refreshPackageInBackground(c, packageName, fetchUpstreamFn, options))
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Background refresh function (fallback when queue is not available)
|
|
142
|
+
*/
|
|
143
|
+
async function refreshPackageInBackground<T>(
|
|
144
|
+
c: HonoContext,
|
|
145
|
+
packageName: string,
|
|
146
|
+
fetchUpstreamFn: () => Promise<T>,
|
|
147
|
+
options: CacheOptions
|
|
148
|
+
): Promise<void> {
|
|
149
|
+
try {
|
|
150
|
+
console.log(`[BACKGROUND] Refreshing package data for: ${packageName}`)
|
|
151
|
+
const upstreamData = await fetchUpstreamFn()
|
|
152
|
+
await cachePackageData(c, packageName, upstreamData, options)
|
|
153
|
+
console.log(`[BACKGROUND] Successfully refreshed package: ${packageName}`)
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error(`[BACKGROUND ERROR] Failed to refresh package ${packageName}: ${(error as Error).message}`)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Enhanced cache data retrieval with stale-while-revalidate support
|
|
161
|
+
*/
|
|
162
|
+
async function getCachedPackageData(
|
|
163
|
+
c: HonoContext,
|
|
164
|
+
packageName: string,
|
|
165
|
+
ttlMinutes: number,
|
|
166
|
+
staleWhileRevalidateMinutes: number
|
|
167
|
+
): Promise<CacheValidation> {
|
|
168
|
+
try {
|
|
169
|
+
const cachedPackage = await c.db.getCachedPackage(packageName)
|
|
170
|
+
|
|
171
|
+
if (!cachedPackage || !cachedPackage.cachedAt || cachedPackage.origin !== 'upstream') {
|
|
172
|
+
return { valid: false, stale: false, data: null }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const cacheTime = new Date(cachedPackage.cachedAt).getTime()
|
|
176
|
+
const now = new Date().getTime()
|
|
177
|
+
const ttlMs = ttlMinutes * 60 * 1000
|
|
178
|
+
const staleMs = staleWhileRevalidateMinutes * 60 * 1000
|
|
179
|
+
|
|
180
|
+
const age = now - cacheTime
|
|
181
|
+
const isValid = age < ttlMs
|
|
182
|
+
const isStale = age < staleMs // Still usable even if stale
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
valid: isValid,
|
|
186
|
+
stale: isStale && !isValid, // Stale means expired but within stale window
|
|
187
|
+
data: cachedPackage,
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error(`[DB ERROR] Failed to get cached package data for ${packageName}: ${(error as Error).message}`)
|
|
191
|
+
return { valid: false, stale: false, data: null }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Stale-while-revalidate cache strategy for version data
|
|
197
|
+
* Returns stale cache immediately and refreshes in background via queue
|
|
198
|
+
*/
|
|
199
|
+
export async function getCachedVersionWithRefresh<T>(
|
|
200
|
+
c: HonoContext,
|
|
201
|
+
spec: string,
|
|
202
|
+
fetchUpstreamFn: () => Promise<T>,
|
|
203
|
+
options: CacheOptions = {}
|
|
204
|
+
): Promise<CacheResult<T>> {
|
|
205
|
+
const {
|
|
206
|
+
manifestTtlMinutes = 525600, // 1 year TTL for manifests
|
|
207
|
+
staleWhileRevalidateMinutes = 1051200, // Allow stale for 2 years (manifests rarely change)
|
|
208
|
+
forceRefresh = false,
|
|
209
|
+
upstream = 'npm'
|
|
210
|
+
} = options
|
|
211
|
+
|
|
212
|
+
// If forcing refresh, skip cache entirely
|
|
213
|
+
if (forceRefresh) {
|
|
214
|
+
console.log(`[CACHE] Force refresh requested for version: ${spec}`)
|
|
215
|
+
const upstreamData = await fetchUpstreamFn()
|
|
216
|
+
if (upstreamData) {
|
|
217
|
+
c.waitUntil?.(cacheVersionData(c, spec, upstreamData, options))
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
version: upstreamData,
|
|
221
|
+
fromCache: false,
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
// Get cached data first
|
|
227
|
+
const cachedResult = await getCachedVersionData(c, spec, manifestTtlMinutes, staleWhileRevalidateMinutes)
|
|
228
|
+
|
|
229
|
+
if (cachedResult.data) {
|
|
230
|
+
const { valid, stale, data } = cachedResult
|
|
231
|
+
|
|
232
|
+
if (valid) {
|
|
233
|
+
// Cache is fresh - return immediately
|
|
234
|
+
console.log(`[CACHE] Using fresh cached version: ${spec}`)
|
|
235
|
+
return {
|
|
236
|
+
version: data as T,
|
|
237
|
+
fromCache: true,
|
|
238
|
+
stale: false
|
|
239
|
+
}
|
|
240
|
+
} else if (stale) {
|
|
241
|
+
// Cache is stale but within stale-while-revalidate window
|
|
242
|
+
// Return stale data immediately and queue background refresh
|
|
243
|
+
console.log(`[CACHE] Using stale data for ${spec}, queuing background refresh`)
|
|
244
|
+
|
|
245
|
+
// Queue background refresh using Cloudflare Queues
|
|
246
|
+
if (c.env.CACHE_REFRESH_QUEUE) {
|
|
247
|
+
await queueVersionRefresh(c, spec, upstream, fetchUpstreamFn, options)
|
|
248
|
+
} else {
|
|
249
|
+
// Fallback to waitUntil if queue not available
|
|
250
|
+
console.log(`[CACHE] Queue not available, using waitUntil fallback for ${spec}`)
|
|
251
|
+
c.waitUntil?.(refreshVersionInBackground(c, spec, fetchUpstreamFn, options))
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
version: data as T,
|
|
256
|
+
fromCache: true,
|
|
257
|
+
stale: true
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// No cache data available - fetch upstream synchronously
|
|
263
|
+
console.log(`[CACHE] No cache available for ${spec}, fetching upstream`)
|
|
264
|
+
const upstreamData = await fetchUpstreamFn()
|
|
265
|
+
|
|
266
|
+
// Cache the fresh data in background
|
|
267
|
+
c.waitUntil?.(cacheVersionData(c, spec, upstreamData, options))
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
version: upstreamData,
|
|
271
|
+
fromCache: false,
|
|
272
|
+
stale: false
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
} catch (error) {
|
|
276
|
+
console.error(`[CACHE ERROR] Failed to get cached version ${spec}: ${(error as Error).message}`)
|
|
277
|
+
throw error
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Queue a version refresh job using Cloudflare Queues
|
|
283
|
+
*/
|
|
284
|
+
async function queueVersionRefresh<T>(
|
|
285
|
+
c: HonoContext,
|
|
286
|
+
spec: string,
|
|
287
|
+
upstream: string,
|
|
288
|
+
fetchUpstreamFn: () => Promise<T>,
|
|
289
|
+
options: CacheOptions
|
|
290
|
+
): Promise<void> {
|
|
291
|
+
try {
|
|
292
|
+
const message: QueueMessage = {
|
|
293
|
+
type: 'version_refresh',
|
|
294
|
+
spec,
|
|
295
|
+
upstream,
|
|
296
|
+
timestamp: Date.now(),
|
|
297
|
+
options: {
|
|
298
|
+
manifestTtlMinutes: options.manifestTtlMinutes || 525600,
|
|
299
|
+
upstream: options.upstream || 'npm'
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
await c.env.CACHE_REFRESH_QUEUE.send(message)
|
|
304
|
+
console.log(`[QUEUE] Queued refresh for version: ${spec}`)
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.error(`[QUEUE ERROR] Failed to queue refresh for ${spec}: ${(error as Error).message}`)
|
|
307
|
+
// Fallback to immediate background refresh
|
|
308
|
+
c.waitUntil?.(refreshVersionInBackground(c, spec, fetchUpstreamFn, options))
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Background refresh function for versions (fallback when queue is not available)
|
|
314
|
+
*/
|
|
315
|
+
async function refreshVersionInBackground<T>(
|
|
316
|
+
c: HonoContext,
|
|
317
|
+
spec: string,
|
|
318
|
+
fetchUpstreamFn: () => Promise<T>,
|
|
319
|
+
options: CacheOptions
|
|
320
|
+
): Promise<void> {
|
|
321
|
+
try {
|
|
322
|
+
console.log(`[BACKGROUND] Refreshing version data for: ${spec}`)
|
|
323
|
+
const upstreamData = await fetchUpstreamFn()
|
|
324
|
+
await cacheVersionData(c, spec, upstreamData, options)
|
|
325
|
+
console.log(`[BACKGROUND] Successfully refreshed version: ${spec}`)
|
|
326
|
+
} catch (error) {
|
|
327
|
+
console.error(`[BACKGROUND ERROR] Failed to refresh version ${spec}: ${(error as Error).message}`)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Enhanced cache data retrieval for versions with stale-while-revalidate support
|
|
333
|
+
*/
|
|
334
|
+
async function getCachedVersionData(
|
|
335
|
+
c: HonoContext,
|
|
336
|
+
spec: string,
|
|
337
|
+
ttlMinutes: number,
|
|
338
|
+
staleWhileRevalidateMinutes: number
|
|
339
|
+
): Promise<CacheValidation> {
|
|
340
|
+
try {
|
|
341
|
+
const cachedVersion = await c.db.getCachedVersion(spec)
|
|
342
|
+
|
|
343
|
+
if (!cachedVersion || !cachedVersion.cachedAt || cachedVersion.origin !== 'upstream') {
|
|
344
|
+
return { valid: false, stale: false, data: null }
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const cacheTime = new Date(cachedVersion.cachedAt).getTime()
|
|
348
|
+
const now = new Date().getTime()
|
|
349
|
+
const ttlMs = ttlMinutes * 60 * 1000
|
|
350
|
+
const staleMs = staleWhileRevalidateMinutes * 60 * 1000
|
|
351
|
+
|
|
352
|
+
const age = now - cacheTime
|
|
353
|
+
const isValid = age < ttlMs
|
|
354
|
+
const isStale = age < staleMs // Still usable even if stale
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
valid: isValid,
|
|
358
|
+
stale: isStale && !isValid, // Stale means expired but within stale window
|
|
359
|
+
data: cachedVersion,
|
|
360
|
+
}
|
|
361
|
+
} catch (error) {
|
|
362
|
+
console.error(`[DB ERROR] Failed to get cached version data for ${spec}: ${(error as Error).message}`)
|
|
363
|
+
return { valid: false, stale: false, data: null }
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Cache package data in the database
|
|
369
|
+
*/
|
|
370
|
+
async function cachePackageData<T>(
|
|
371
|
+
c: HonoContext,
|
|
372
|
+
packageName: string,
|
|
373
|
+
packageData: T,
|
|
374
|
+
options: CacheOptions
|
|
375
|
+
): Promise<void> {
|
|
376
|
+
try {
|
|
377
|
+
if (packageData && typeof packageData === 'object' && 'tags' in packageData) {
|
|
378
|
+
const tags = (packageData as any).tags || {}
|
|
379
|
+
await c.db.upsertCachedPackage(packageName, tags, options.upstream || 'npm')
|
|
380
|
+
console.log(`[CACHE] Successfully cached package data: ${packageName}`)
|
|
381
|
+
}
|
|
382
|
+
} catch (error) {
|
|
383
|
+
console.error(`[CACHE ERROR] Failed to cache package data for ${packageName}: ${(error as Error).message}`)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Cache version data in the database
|
|
389
|
+
*/
|
|
390
|
+
async function cacheVersionData<T>(
|
|
391
|
+
c: HonoContext,
|
|
392
|
+
spec: string,
|
|
393
|
+
versionData: T,
|
|
394
|
+
options: CacheOptions
|
|
395
|
+
): Promise<void> {
|
|
396
|
+
try {
|
|
397
|
+
if (versionData && typeof versionData === 'object') {
|
|
398
|
+
const manifest = versionData as unknown as PackageManifest
|
|
399
|
+
const publishedAt = new Date().toISOString()
|
|
400
|
+
await c.db.upsertCachedVersion(spec, manifest, options.upstream || 'npm', publishedAt)
|
|
401
|
+
console.log(`[CACHE] Successfully cached version data: ${spec}`)
|
|
402
|
+
}
|
|
403
|
+
} catch (error) {
|
|
404
|
+
console.error(`[CACHE ERROR] Failed to cache version data for ${spec}: ${(error as Error).message}`)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Generate storage path for tarball cache
|
|
410
|
+
*/
|
|
411
|
+
export function getTarballStoragePath(
|
|
412
|
+
packageName: string,
|
|
413
|
+
version: string,
|
|
414
|
+
origin: 'local' | 'upstream' = 'local',
|
|
415
|
+
upstream: string | null = null
|
|
416
|
+
): string {
|
|
417
|
+
const sanitizedName = packageName.replace(/[@\/]/g, '_')
|
|
418
|
+
const sanitizedVersion = version.replace(/[^a-zA-Z0-9.-]/g, '_')
|
|
419
|
+
|
|
420
|
+
if (origin === 'upstream' && upstream) {
|
|
421
|
+
return `tarballs/${upstream}/${sanitizedName}/${sanitizedVersion}.tgz`
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return `tarballs/local/${sanitizedName}/${sanitizedVersion}.tgz`
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Check if tarball is cached
|
|
429
|
+
*/
|
|
430
|
+
export async function isTarballCached(
|
|
431
|
+
c: HonoContext,
|
|
432
|
+
packageName: string,
|
|
433
|
+
version: string,
|
|
434
|
+
origin: 'local' | 'upstream' = 'local',
|
|
435
|
+
upstream: string | null = null
|
|
436
|
+
): Promise<boolean> {
|
|
437
|
+
try {
|
|
438
|
+
const storagePath = getTarballStoragePath(packageName, version, origin, upstream)
|
|
439
|
+
// This would need to be implemented based on your storage solution
|
|
440
|
+
// For now, return false as a placeholder
|
|
441
|
+
return false
|
|
442
|
+
} catch (error) {
|
|
443
|
+
console.error(`[CACHE ERROR] Failed to check tarball cache: ${(error as Error).message}`)
|
|
444
|
+
return false
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Cache tarball data
|
|
450
|
+
*/
|
|
451
|
+
export async function cacheTarball(
|
|
452
|
+
c: HonoContext,
|
|
453
|
+
packageName: string,
|
|
454
|
+
version: string,
|
|
455
|
+
tarballStream: ReadableStream,
|
|
456
|
+
origin: 'local' | 'upstream' = 'local',
|
|
457
|
+
upstream: string | null = null
|
|
458
|
+
): Promise<void> {
|
|
459
|
+
try {
|
|
460
|
+
const storagePath = getTarballStoragePath(packageName, version, origin, upstream)
|
|
461
|
+
// This would need to be implemented based on your storage solution
|
|
462
|
+
console.log(`[CACHE] Would cache tarball at: ${storagePath}`)
|
|
463
|
+
} catch (error) {
|
|
464
|
+
console.error(`[CACHE ERROR] Failed to cache tarball: ${(error as Error).message}`)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createDatabaseOperations } from '../db/client.ts'
|
|
2
|
+
import type { D1Database } from '@cloudflare/workers-types'
|
|
3
|
+
import type { DatabaseOperations } from '../../types.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates database operations instance from D1 database
|
|
7
|
+
* @param d1 - The D1 database instance
|
|
8
|
+
* @returns Database operations interface
|
|
9
|
+
*/
|
|
10
|
+
export function createDB(d1: D1Database) {
|
|
11
|
+
return createDatabaseOperations(d1)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Type guard to check if database is available
|
|
16
|
+
* @param db - The database instance to check
|
|
17
|
+
* @returns True if database is available and functional
|
|
18
|
+
*/
|
|
19
|
+
export function isDatabaseAvailable(db: any): db is DatabaseOperations {
|
|
20
|
+
return db && typeof db.getPackage === 'function'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Cache the database operations to avoid recreating on every request
|
|
24
|
+
let cachedDbOperations: any = null
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Middleware to mount database operations on the context
|
|
28
|
+
* @param c - The Hono context
|
|
29
|
+
* @param next - The next middleware function
|
|
30
|
+
*/
|
|
31
|
+
export async function mountDatabase(c: any, next: any) {
|
|
32
|
+
if (!c.env.DB) {
|
|
33
|
+
throw new Error('Database not found in environment')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Reuse existing database operations if available
|
|
37
|
+
if (!cachedDbOperations) {
|
|
38
|
+
console.log('[DB] Creating new database operations instance')
|
|
39
|
+
cachedDbOperations = createDatabaseOperations(c.env.DB)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
c.db = cachedDbOperations as any
|
|
43
|
+
await next()
|
|
44
|
+
}
|