@ubean/server 0.1.13 → 0.2.1

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.
Files changed (56) hide show
  1. package/dist/analytics-entry.d.ts +2 -0
  2. package/dist/analytics-entry.js +2 -0
  3. package/dist/cache-C84ix1Vq.js +173 -0
  4. package/dist/cache-b-MZlyv0.d.ts +48 -0
  5. package/dist/cache-directive-C1Nekkza.js +304 -0
  6. package/dist/cache-directive-CAxJAQyE.d.ts +175 -0
  7. package/dist/cache-directive.d.ts +2 -0
  8. package/dist/cache-directive.js +2 -0
  9. package/dist/cache-entry.d.ts +3 -0
  10. package/dist/cache-entry.js +3 -0
  11. package/dist/cron-entry.d.ts +2 -0
  12. package/dist/cron-entry.js +2 -0
  13. package/dist/cron-scheduler-BF33PPn4.d.ts +77 -0
  14. package/dist/cron-scheduler-BVuXv7nn.js +258 -0
  15. package/dist/database-CfpFznl-.d.ts +67 -0
  16. package/dist/database-DNrY44SQ.js +352 -0
  17. package/dist/database.d.ts +2 -0
  18. package/dist/database.js +2 -0
  19. package/dist/email-BjfRiR9b.js +354 -0
  20. package/dist/email-BvpEuNn_.d.ts +226 -0
  21. package/dist/email.d.ts +2 -0
  22. package/dist/email.js +2 -0
  23. package/dist/feature-flags-CdLwsMD2.js +657 -0
  24. package/dist/feature-flags-DWkS6p0D.d.ts +386 -0
  25. package/dist/fetch-memo-rbkxxnW4.js +338 -0
  26. package/dist/index.d.ts +183 -488
  27. package/dist/index.js +352 -2023
  28. package/dist/middleware.d.ts +2 -0
  29. package/dist/middleware.js +3 -0
  30. package/dist/observability-Cio6Qq1H.js +339 -0
  31. package/dist/observability-DUNUEjj3.d.ts +70 -0
  32. package/dist/observability.d.ts +2 -0
  33. package/dist/observability.js +2 -0
  34. package/dist/queue-Bwzi3mhK.js +210 -0
  35. package/dist/queue-GOfTAWlz.d.ts +55 -0
  36. package/dist/queue.d.ts +2 -0
  37. package/dist/queue.js +2 -0
  38. package/dist/realtime.d.ts +2 -0
  39. package/dist/realtime.js +2 -0
  40. package/dist/security.d.ts +2 -0
  41. package/dist/security.js +2 -0
  42. package/dist/sessions-BLqFFQTL.d.ts +217 -0
  43. package/dist/sessions-BsBsyFAG.js +450 -0
  44. package/dist/single-flight-BJyhDLdU.d.ts +422 -0
  45. package/dist/single-flight-mJ4ZKbx1.js +715 -0
  46. package/dist/sse-Ct72zhic.d.ts +95 -0
  47. package/dist/sse-a6Ky9Vcl.js +310 -0
  48. package/dist/static-DPHaovQe.js +90 -0
  49. package/dist/static-K2dRvjpS.d.ts +11 -0
  50. package/dist/static.d.ts +2 -0
  51. package/dist/static.js +2 -0
  52. package/dist/storage-BZLMaqHr.js +162 -0
  53. package/dist/storage-QdlPtPtR.d.ts +48 -0
  54. package/dist/storage.d.ts +2 -0
  55. package/dist/storage.js +2 -0
  56. package/package.json +68 -6
package/dist/index.js CHANGED
@@ -1,2066 +1,395 @@
1
- import { createHooks } from "hookable";
2
- import { existsSync, readFileSync, statSync } from "node:fs";
3
- import { extname, join } from "pathe";
4
- //#region src/cache.ts
5
- function createMemoryStore(maxEntries = 200) {
6
- const store = /* @__PURE__ */ new Map();
7
- function evict() {
8
- if (store.size <= maxEntries) return;
9
- const entries = Array.from(store.entries()).sort((a, b) => a[1].createdAt - b[1].createdAt);
10
- const removeCount = Math.ceil(entries.length * .2);
11
- for (let i = 0; i < removeCount; i++) store.delete(entries[i][0]);
1
+ import { a as invalidateRouteCache, i as createMemoryStore, n as clearCacheStore, o as resolveRouteCacheRules, r as createCacheMiddleware, s as useCacheStore, t as cachedEventHandler } from "./cache-C84ix1Vq.js";
2
+ import { a as rawSql, c as useDatabase, i as migrateDatabase, n as defineDatabase, o as registerDb0Create, r as getDatabaseHooks, s as runMigrations, t as closeDatabases } from "./database-DNrY44SQ.js";
3
+ import { a as getQueueDefinitions, c as sendMessages, d as stopQueueWorkers, f as useQueueDriver, i as getAllQueueStats, l as setQueueDriver, n as createMemoryQueueDriver, o as getQueueStats, r as defineQueue, s as sendMessage, t as clearQueueDefinitions, u as startQueueWorkers } from "./queue-Bwzi3mhK.js";
4
+ import { a as validateCron, c as defineScheduled, i as startCronScheduler, l as getScheduledTasks, n as parseCron, o as clearScheduledTasks, r as resetCronRunCounts, s as createCronContext, t as createMemoryCronScheduler, u as runScheduledTask } from "./cron-scheduler-BVuXv7nn.js";
5
+ import { S as registerWebSocket, _ as getWebSocketDefinitions, a as defineSSE, b as handleMessage, c as sseHeaders, d as createRoom, f as createWebSocketMiddleware, g as getRooms, h as getRoom, i as createSSEStream, l as broadcast, m as defineWebSocket, n as clearSSEState, o as formatSSEMessage, p as defineRoom, r as closeAllSSE, s as getSSEConnections, t as broadcastSSE, u as clearWebSocketState, v as handleClose, x as handleUpgrade, y as handleError } from "./sse-a6Ky9Vcl.js";
6
+ import { a as useKV, i as createStorage, n as createKV, o as useStorage, r as createMemoryDriver, t as clearGlobalStorage } from "./storage-BZLMaqHr.js";
7
+ import { a as getEmailProvider, c as getSentEmails, d as setDefaultEmailProvider, f as useEmailProvider, i as defineEmailProvider, l as renderEmailTemplate, n as clearSentEmails, o as getEmailProviderNames, r as createEmailTransport, s as getEmailTransport, t as clearEmailProviders, u as sendEmail } from "./email-BjfRiR9b.js";
8
+ import { a as createSpan, c as getGlobalTracer, d as setGlobalTracer, f as startSpan, i as createOpenTelemetryExporter, l as getRequestId, n as createConsoleExporter, o as createTracingMiddleware, p as withSpan, r as createObservabilityTracer, s as generateRequestId, t as REQUEST_ID_HEADER, u as getSpan } from "./observability-Cio6Qq1H.js";
9
+ import { t as serveStatic } from "./static-DPHaovQe.js";
10
+ import { C as defineRateLimit, S as createRateLimitMiddleware, T as defineCors, _ as after, a as getInvalidatedKeys, b as getAfterCallbackCount, c as invalidateKey, d as createDraftModeMiddleware, f as defineDraftMode, g as useDraftMode, h as isDraftMode, i as defineSingleFlight, l as runRevalidation, m as enableDraftMode, n as createSingleFlightMiddleware, o as getRevalidationEntries, p as disableDraftMode, r as defineRevalidation, s as invalidate, t as clearRevalidationRegistry, u as unregisterRevalidation, v as createAfterMiddleware, w as createCorsMiddleware, x as createMemoryRateLimitStore, y as flushAfterCallbacks } from "./single-flight-mJ4ZKbx1.js";
11
+ import { a as createSecurityHeadersMiddleware, c as createCsrfMiddleware, i as useSession, l as defineCsrf, n as createStorageSessionStore, o as defineSecurityHeaders, r as defineSessionStore, s as serializeCsp, t as createSessionMiddleware, u as generateCsrfToken } from "./sessions-BsBsyFAG.js";
12
+ import { a as getDataCacheSize, i as createMemoizedFetch, n as createDataCacheMiddleware, o as revalidateDataCachePath, r as createFetchMemoizationMiddleware, s as revalidateDataCacheTag, t as clearFetchDataCache } from "./fetch-memo-rbkxxnW4.js";
13
+ import { a as createComponentMemoryStore, c as revalidateTag, d as wrapWithCache, i as clearComponentCacheStore, l as revalidateTags, n as cacheTag, o as defineCachedFunction, r as clearComponentCache, s as revalidatePath, t as cacheLife, u as useComponentCacheStore } from "./cache-directive-C1Nekkza.js";
14
+ import { A as getAnalyticsProvider, C as createAnalyticsMiddleware, D as defineAnalytics, E as createMockAnalyticsProvider, F as trackEvent, I as trackPageView, L as trackRaw, M as listAnalyticsProviders, N as registerAnalyticsProvider, O as defineAnalyticsProvider, P as setGlobalAnalyticsProvider, R as unregisterAnalyticsProvider, S as clearAnalyticsProviders, T as createMemoryAnalyticsProvider, _ as removeFeatureFlag, a as defineFeatureFlag, b as useFlagContext, c as evaluateFlagWithReason, d as getVariant, f as getVariantAssignment, g as removeExperiment, h as listFlagNames, i as defineExperiment, j as getGlobalAnalyticsProvider, k as extractAnalyticsContext, l as extractFlagContext, m as listExperimentNames, n as createFeatureFlagsMiddleware, o as evaluateFlag, p as getVariantFromContext, r as createMemoryFeatureFlagStore, s as evaluateFlagFromContext, t as clearFeatureFlags, u as getGlobalFeatureFlagStore, v as setGlobalFeatureFlagStore, w as createLogAnalyticsProvider, x as useFlags, y as useExperiments, z as useAnalytics } from "./feature-flags-CdLwsMD2.js";
15
+ //#region src/cdn-cache.ts
16
+ /**
17
+ * 为当前响应设置 surrogate key 头部。
18
+ *
19
+ * 在路由处理器中调用,为响应打上缓存标签,
20
+ * 后续可通过 `purgeKeys(adapter, keys)` 批量失效。
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * app.get('/products/:id', async (c) => {
25
+ * const product = await getProduct(c.req.param('id'));
26
+ * defineSurrogateKeys(c, [`product:${product.id}`, 'product-list']);
27
+ * return c.json(product);
28
+ * });
29
+ * ```
30
+ */
31
+ function defineSurrogateKeys(c, keys, header = "Cache-Tag") {
32
+ if (keys.length === 0) return;
33
+ c.header(header, keys.join(","));
34
+ }
35
+ /**
36
+ * 从请求上下文读取 surrogate keys(通常用于测试/调试)。
37
+ */
38
+ function getSurrogateKeys(c, header = "Cache-Tag") {
39
+ const value = c.res?.headers.get(header) || c.req.header(header.toLowerCase());
40
+ if (!value) return [];
41
+ return value.split(",").map((k) => k.trim()).filter(Boolean);
42
+ }
43
+ let globalPurgeAdapter = null;
44
+ /**
45
+ * 设置全局默认 CDN 清除适配器。
46
+ */
47
+ function setGlobalPurgeAdapter(adapter) {
48
+ globalPurgeAdapter = adapter;
49
+ }
50
+ /**
51
+ * 获取全局默认清除适配器。
52
+ */
53
+ function getGlobalPurgeAdapter() {
54
+ return globalPurgeAdapter;
55
+ }
56
+ /**
57
+ * 使用指定适配器(或全局适配器)按 key 清除 CDN 缓存。
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * // 数据变更后失效相关页面缓存
62
+ * await purgeKeys(['product:42', 'product-list']);
63
+ * ```
64
+ */
65
+ async function purgeKeys(adapterOrKeys, keys) {
66
+ let adapter;
67
+ let keysToPurge;
68
+ if (Array.isArray(adapterOrKeys)) {
69
+ adapter = globalPurgeAdapter;
70
+ keysToPurge = adapterOrKeys;
71
+ } else {
72
+ adapter = adapterOrKeys;
73
+ keysToPurge = keys || [];
12
74
  }
13
- return {
14
- async get(key) {
15
- const entry = store.get(key);
16
- if (!entry) return void 0;
17
- if (Date.now() > entry.expiresAt) {
18
- store.delete(key);
19
- return;
20
- }
21
- return entry;
22
- },
23
- async set(key, entry, ttl) {
24
- const now = Date.now();
25
- store.set(key, {
26
- ...entry,
27
- createdAt: now,
28
- expiresAt: now + ttl * 1e3
29
- });
30
- evict();
31
- },
32
- async delete(key) {
33
- return store.delete(key);
34
- },
35
- async clear() {
36
- store.clear();
37
- }
75
+ if (!adapter) return {
76
+ success: false,
77
+ purged: 0,
78
+ error: "No CDN purge adapter configured"
38
79
  };
39
- }
40
- let globalStore = null;
41
- function useCacheStore(store) {
42
- if (store) {
43
- globalStore = store;
44
- return store;
45
- }
46
- if (!globalStore) globalStore = createMemoryStore();
47
- return globalStore;
48
- }
49
- function clearCacheStore() {
50
- globalStore = null;
51
- }
52
- function buildCacheKey(c) {
53
- const url = new URL(c.req.url);
54
- return `${c.req.method}:${url.pathname}${url.search}`;
55
- }
56
- function serializeResponse(res) {
57
- return res.arrayBuffer().then((body) => {
58
- const headers = {};
59
- res.headers.forEach((v, k) => {
60
- const kl = k.toLowerCase();
61
- if (kl !== "set-cookie" && kl !== "authorization") headers[k] = v;
62
- });
63
- return {
64
- body,
65
- headers,
66
- status: res.status,
67
- statusText: res.statusText
68
- };
69
- });
70
- }
71
- function entryToResponse(entry) {
72
- const headers = new Headers(entry.headers);
73
- headers.set("X-Cache", "HIT");
74
- headers.set("Age", String(Math.max(0, Math.floor((Date.now() - entry.createdAt) / 1e3))));
75
- return new Response(entry.body, {
76
- status: entry.status,
77
- statusText: entry.statusText,
78
- headers
79
- });
80
- }
81
- function isCacheableRequest(c) {
82
- if (c.req.method !== "GET" && c.req.method !== "HEAD") return false;
83
- if (c.req.header("authorization")) return false;
84
- if (c.req.header("cookie")) {
85
- if (!(c.req.header("cache-control") || "").includes("public")) return false;
86
- }
87
- const cc = c.req.header("cache-control");
88
- if (cc && (cc.includes("no-cache") || cc.includes("no-store"))) return false;
89
- return true;
90
- }
91
- function isCacheableResponse(res) {
92
- if (res.status !== 200) return false;
93
- if (res.headers.get("set-cookie")) return false;
94
- const cc = res.headers.get("cache-control");
95
- if (cc && (cc.includes("private") || cc.includes("no-store"))) return false;
96
- return true;
97
- }
98
- function matchPath(pattern, path) {
99
- const DOUBLE = "__DWC__";
100
- const SINGLE = "__SWC__";
101
- let s = pattern;
102
- s = s.replace(/\/\*\*/g, `/${DOUBLE}`);
103
- s = s.replace(/\*/g, SINGLE);
104
- s = s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
105
- s = s.replace(new RegExp(SINGLE, "g"), "[^/]*");
106
- s = s.replace(new RegExp(`/${DOUBLE}`, "g"), "(?:/.*)?");
107
- return new RegExp(`^${s}$`).test(path);
108
- }
109
- function createCacheMiddleware(options = {}) {
110
- const store = options.store || useCacheStore();
111
- function findTtl(path) {
112
- if (!options.rules) return null;
113
- const patterns = Object.keys(options.rules);
114
- for (const pattern of patterns) if (matchPath(pattern, path)) return options.rules[pattern].ttl;
115
- return null;
116
- }
117
- return async function cacheMiddleware(c, next) {
118
- const path = new URL(c.req.url).pathname;
119
- const ttl = findTtl(path);
120
- if (ttl != null && ttl > 0 && isCacheableRequest(c)) {
121
- const key = buildCacheKey(c);
122
- const existing = await store.get(key);
123
- if (existing) return entryToResponse(existing);
124
- }
125
- await next();
126
- if (ttl == null || ttl <= 0) return;
127
- const res = c.res;
128
- if (!res || !isCacheableRequest(c) || !isCacheableResponse(res)) return;
129
- const serialized = await serializeResponse(res.clone());
130
- await store.set(buildCacheKey(c), serialized, ttl);
80
+ if (keysToPurge.length === 0) return {
81
+ success: true,
82
+ purged: 0
131
83
  };
132
- }
133
- function cachedEventHandler(handler, options = { ttl: 60 }) {
134
- const store = useCacheStore();
135
- const ttl = options.ttl;
136
- const keyBase = options.name;
137
- return (async (c) => {
138
- const key = keyBase || buildCacheKey(c);
139
- if (ttl > 0 && isCacheableRequest(c)) {
140
- const existing = await store.get(key);
141
- if (existing) return entryToResponse(existing);
142
- }
143
- const res = await handler(c);
144
- if (ttl > 0 && isCacheableResponse(res)) {
145
- const serialized = await serializeResponse(res.clone());
146
- await store.set(key, serialized, ttl);
147
- }
148
- return res;
149
- });
150
- }
151
- async function invalidateRouteCache(keyPattern) {
152
- const store = useCacheStore();
153
- if (!keyPattern) {
154
- await store.clear();
155
- return;
156
- }
157
- if (typeof keyPattern === "string") {
158
- await store.delete(keyPattern);
159
- return;
160
- }
161
- const memStore = store;
162
- if (memStore.store) {
163
- for (const k of Array.from(memStore.store.keys())) if (keyPattern.test(k)) await store.delete(k);
84
+ return adapter.purgeKeys(keysToPurge);
85
+ }
86
+ /**
87
+ * URL 清除 CDN 缓存。
88
+ */
89
+ async function purgeUrls(adapterOrUrls, urls) {
90
+ let adapter;
91
+ let urlsToPurge;
92
+ if (Array.isArray(adapterOrUrls)) {
93
+ adapter = globalPurgeAdapter;
94
+ urlsToPurge = adapterOrUrls;
95
+ } else {
96
+ adapter = adapterOrUrls;
97
+ urlsToPurge = urls || [];
164
98
  }
165
- }
166
- function resolveRouteCacheRules(routeRules) {
167
- const result = {};
168
- for (const [path, rule] of Object.entries(routeRules)) if (rule.cache && rule.cache.ttl != null) result[path] = {
169
- ttl: rule.cache.ttl,
170
- swr: rule.cache.swr
99
+ if (!adapter) return {
100
+ success: false,
101
+ purged: 0,
102
+ error: "No CDN purge adapter configured"
171
103
  };
172
- return result;
173
- }
174
- //#endregion
175
- //#region src/database.ts
176
- const _global = globalThis;
177
- const dbRegistry = /* @__PURE__ */ new Map();
178
- const dbHooks = createHooks();
179
- let defaultDatabase = null;
180
- const RAW_SQL = Symbol("rawSql");
181
- function rawSql(str) {
182
- return {
183
- [RAW_SQL]: true,
184
- value: str
104
+ if (!adapter.purgeUrls) return {
105
+ success: false,
106
+ purged: 0,
107
+ error: `Adapter "${adapter.name}" does not support URL purging`
185
108
  };
186
- }
187
- function createInMemoryDatabase() {
188
- const tables = /* @__PURE__ */ new Map();
189
- function getTable(name) {
190
- return tables.get(name.toLowerCase());
191
- }
192
- function parseSqlValue(val) {
193
- const trimmed = val.trim();
194
- const upper = trimmed.toUpperCase();
195
- if (upper === "NULL") return null;
196
- if (upper === "TRUE") return true;
197
- if (upper === "FALSE") return false;
198
- if (/^-?\d+$/.test(trimmed)) return parseInt(trimmed, 10);
199
- if (/^-?\d+\.\d+$/.test(trimmed)) return parseFloat(trimmed);
200
- if (trimmed.startsWith("'") && trimmed.endsWith("'") || trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
201
- const quote = trimmed[0];
202
- return trimmed.slice(1, -1).replace(new RegExp(quote + quote, "g"), quote);
203
- }
204
- return trimmed;
205
- }
206
- function splitByCommaRespectingStrings(str) {
207
- const parts = [];
208
- let current = "";
209
- let inString = false;
210
- let stringChar = "";
211
- let depth = 0;
212
- for (let i = 0; i < str.length; i++) {
213
- const ch = str[i];
214
- const next = str[i + 1];
215
- if (!inString && (ch === "'" || ch === "\"")) {
216
- inString = true;
217
- stringChar = ch;
218
- current += ch;
219
- } else if (inString && ch === stringChar) if (next === stringChar) {
220
- current += ch + ch;
221
- i++;
222
- } else {
223
- inString = false;
224
- current += ch;
225
- }
226
- else if (!inString && ch === "(") {
227
- depth++;
228
- current += ch;
229
- } else if (!inString && ch === ")") {
230
- depth--;
231
- current += ch;
232
- } else if (!inString && depth === 0 && ch === ",") {
233
- parts.push(current.trim());
234
- current = "";
235
- } else current += ch;
236
- }
237
- if (current.trim()) parts.push(current.trim());
238
- return parts;
239
- }
240
- function parseCreateTable(query) {
241
- const ifNotExists = /IF\s+NOT\s+EXISTS/i.test(query);
242
- const match = query.match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?(\w+)["`]?\s*\(([\s\S]+)\)\s*;?\s*$/i);
243
- if (!match) return;
244
- const [, tableName, colsStr] = match;
245
- const lowerName = tableName.toLowerCase();
246
- if (ifNotExists && tables.has(lowerName)) return;
247
- const table = {
248
- name: tableName,
249
- columns: /* @__PURE__ */ new Map(),
250
- rows: []
251
- };
252
- const colDefs = splitByCommaRespectingStrings(colsStr);
253
- for (const colDef of colDefs) {
254
- const upperDef = colDef.toUpperCase().trim();
255
- if (upperDef.startsWith("PRIMARY KEY") || upperDef.startsWith("CONSTRAINT") || upperDef.startsWith("FOREIGN") || upperDef.startsWith("UNIQUE") || upperDef.startsWith("INDEX")) continue;
256
- const match2 = colDef.match(/^["`]?(\w+)["`]?\s+(\w+)?(.*)$/);
257
- if (!match2) continue;
258
- const [, colNameRaw, colTypeRaw = "TEXT", rest] = match2;
259
- const colName = colNameRaw.replace(/["`]/g, "");
260
- const colType = (colTypeRaw || "TEXT").toUpperCase();
261
- const isPK = /PRIMARY\s+KEY/i.test(rest);
262
- const notNull = /NOT\s+NULL/i.test(rest);
263
- table.columns.set(colName, {
264
- type: colType,
265
- primaryKey: isPK,
266
- notNull
267
- });
268
- }
269
- tables.set(lowerName, table);
270
- }
271
- function parseInsert(query) {
272
- const insertMatch = query.match(/INSERT\s+INTO\s+["`]?(\w+)["`]?\s*(?:\(([^)]+)\))?\s*VALUES\s*\(([\s\S]+)\)\s*;?\s*$/i);
273
- if (!insertMatch) return null;
274
- const [, tableName, colsStr, valuesStr] = insertMatch;
275
- const table = getTable(tableName);
276
- if (!table) return null;
277
- const columns = colsStr ? splitByCommaRespectingStrings(colsStr).map((c) => c.trim().replace(/["`]/g, "")) : Array.from(table.columns.keys());
278
- const values = splitByCommaRespectingStrings(valuesStr).map((s) => parseSqlValue(s));
279
- return {
280
- table: tableName.toLowerCase(),
281
- columns,
282
- values
283
- };
284
- }
285
- function parseSelect(query) {
286
- const simpleMatch = query.match(/SELECT\s+([\s\S]+?)\s+FROM\s+["`]?(\w+)["`]?/i);
287
- if (!simpleMatch) return null;
288
- const tableName = simpleMatch[2];
289
- let where;
290
- const whereIdx = query.toUpperCase().indexOf("WHERE");
291
- if (whereIdx !== -1) {
292
- let wherePart = query.slice(whereIdx + 5);
293
- const orderIdx = wherePart.toUpperCase().indexOf("ORDER");
294
- const limitIdx = wherePart.toUpperCase().indexOf("LIMIT");
295
- let endIdx = wherePart.length;
296
- if (orderIdx !== -1) endIdx = Math.min(endIdx, orderIdx);
297
- if (limitIdx !== -1) endIdx = Math.min(endIdx, limitIdx);
298
- wherePart = wherePart.slice(0, endIdx).trim().replace(/;$/, "");
299
- const eqIdx = wherePart.indexOf("=");
300
- if (eqIdx !== -1) where = {
301
- col: wherePart.slice(0, eqIdx).trim().replace(/["`]/g, ""),
302
- val: parseSqlValue(wherePart.slice(eqIdx + 1).trim())
303
- };
304
- }
305
- return {
306
- table: tableName.toLowerCase(),
307
- where
308
- };
309
- }
310
- function escapeValue(val) {
311
- if (val !== null && typeof val === "object" && RAW_SQL in val) return val.value;
312
- if (val === null || val === void 0) return "NULL";
313
- if (typeof val === "number") return String(val);
314
- if (typeof val === "boolean") return val ? "1" : "0";
315
- if (val instanceof Date) return `'${val.toISOString()}'`;
316
- return `'${String(val).replace(/'/g, "''")}'`;
317
- }
318
- function buildQueryFromTemplate(strings, values) {
319
- let query = "";
320
- for (let i = 0; i < strings.length; i++) {
321
- query += strings[i];
322
- if (i < values.length) query += escapeValue(values[i]);
323
- }
324
- return query;
325
- }
326
- function executeRaw(query) {
327
- const upperQuery = query.toUpperCase().trim();
328
- if (upperQuery.startsWith("CREATE TABLE")) {
329
- parseCreateTable(query);
330
- return [];
331
- }
332
- if (upperQuery.startsWith("INSERT INTO")) {
333
- const parsed = parseInsert(query);
334
- if (parsed) {
335
- const table = getTable(parsed.table);
336
- if (table) {
337
- const row = {};
338
- for (let i = 0; i < parsed.columns.length; i++) row[parsed.columns[i]] = parsed.values[i] ?? null;
339
- table.rows.push(row);
340
- }
341
- }
342
- return [];
343
- }
344
- if (upperQuery.startsWith("SELECT")) {
345
- const parsed = parseSelect(query);
346
- if (parsed) {
347
- const table = getTable(parsed.table);
348
- if (!table) return [];
349
- let rows = [...table.rows];
350
- if (parsed.where) rows = rows.filter((row) => row[parsed.where.col] == parsed.where.val);
351
- return rows;
352
- }
353
- return [];
354
- }
355
- if (upperQuery.startsWith("DELETE FROM")) {
356
- const match = query.match(/DELETE\s+FROM\s+["`]?(\w+)["`]?\s*(?:WHERE\s+(.+))?/i);
357
- if (match) {
358
- const table = getTable(match[1]);
359
- if (table) {
360
- const whereStr = match[2];
361
- if (whereStr) {
362
- const eqIdx = whereStr.indexOf("=");
363
- if (eqIdx !== -1) {
364
- const col = whereStr.slice(0, eqIdx).trim().replace(/["`]/g, "");
365
- const val = parseSqlValue(whereStr.slice(eqIdx + 1).trim());
366
- table.rows = table.rows.filter((row) => row[col] != val);
367
- } else table.rows = [];
368
- } else table.rows = [];
369
- }
370
- }
371
- return [];
372
- }
373
- if (upperQuery.startsWith("DROP TABLE")) {
374
- const match = query.match(/DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?["`]?(\w+)["`]?/i);
375
- if (match) tables.delete(match[1].toLowerCase());
376
- return [];
377
- }
378
- return [];
379
- }
380
- return {
381
- async sql(strings, ...values) {
382
- const query = buildQueryFromTemplate(strings, values);
383
- try {
384
- await dbHooks.callHook("db:query", query, values);
385
- return { rows: executeRaw(query) };
386
- } catch (err) {
387
- await dbHooks.callHook("db:error", err instanceof Error ? err : new Error(String(err)), query);
388
- throw err;
389
- }
390
- },
391
- async exec(query) {
392
- const statements = splitSqlStatements(query);
393
- for (const stmt of statements) {
394
- const trimmed = stmt.trim();
395
- if (!trimmed) continue;
396
- try {
397
- await dbHooks.callHook("db:query", trimmed);
398
- executeRaw(trimmed);
399
- } catch (err) {
400
- await dbHooks.callHook("db:error", err instanceof Error ? err : new Error(String(err)), trimmed);
401
- throw err;
402
- }
403
- }
404
- },
405
- async close() {
406
- tables.clear();
407
- }
109
+ if (urlsToPurge.length === 0) return {
110
+ success: true,
111
+ purged: 0
408
112
  };
409
- }
410
- function splitSqlStatements(sql) {
411
- const statements = [];
412
- let current = "";
413
- let inString = false;
414
- let stringChar = "";
415
- for (let i = 0; i < sql.length; i++) {
416
- const ch = sql[i];
417
- if (!inString && (ch === "'" || ch === "\"")) {
418
- inString = true;
419
- stringChar = ch;
420
- current += ch;
421
- } else if (inString && ch === stringChar) if (sql[i + 1] === stringChar) {
422
- current += ch + ch;
423
- i++;
424
- } else {
425
- inString = false;
426
- current += ch;
427
- }
428
- else if (!inString && ch === ";") {
429
- statements.push(current);
430
- current = "";
431
- } else current += ch;
432
- }
433
- if (current.trim()) statements.push(current);
434
- return statements;
435
- }
436
- function wrapDatabase(raw, name) {
437
- const db = {
438
- sql: raw.sql,
439
- exec: raw.exec,
440
- async close() {
441
- await raw.close();
442
- if (name) dbRegistry.delete(name);
443
- if (defaultDatabase === db) defaultDatabase = null;
444
- await dbHooks.callHook("db:disconnect", db);
113
+ return adapter.purgeUrls(urlsToPurge);
114
+ }
115
+ /**
116
+ * 创建 Cache-Control 头部生成中间件。
117
+ *
118
+ * 根据路由规则自动设置 `Cache-Control` 响应头,
119
+ * 支持按路径模式自定义 max-age / s-maxage / immutable 等。
120
+ *
121
+ * @example
122
+ * ```typescript
123
+ * app.use('*', createCacheControlMiddleware({
124
+ * defaultMaxAge: 60,
125
+ * sMaxAge: 300,
126
+ * rules: [
127
+ * { pattern: '/_assets/**', maxAge: 31536000, immutable: true },
128
+ * { pattern: '/api/**', maxAge: 0 }
129
+ * ],
130
+ * exclude: ['/api/health', '/api/webhook']
131
+ * }));
132
+ * ```
133
+ */
134
+ function createCacheControlMiddleware(options = {}) {
135
+ const { defaultMaxAge, sMaxAge, staleWhileRevalidate, staleIfError, public: isPublic = true, immutable = false, rules = [], exclude = [], noCacheOnError = true } = options;
136
+ return async function cacheControlMiddleware(c, next) {
137
+ const path = c.req.path;
138
+ if (exclude.some((prefix) => path.startsWith(prefix))) {
139
+ await next();
140
+ return;
445
141
  }
446
- };
447
- return db;
448
- }
449
- function defineDatabase(options = {}) {
450
- if (!options.connector && !options.connectors) {
451
- const db = wrapDatabase(createInMemoryDatabase());
452
- if (!defaultDatabase) defaultDatabase = db;
453
- dbRegistry.set("default", db);
454
- dbHooks.callHook("db:connect", db);
455
- return db;
456
- }
457
- const createFn = _global.$db0Create;
458
- let raw;
459
- if (options.connector) if (createFn) try {
460
- raw = createFn(options.connector);
461
- } catch {
462
- raw = createInMemoryDatabase();
463
- }
464
- else raw = createInMemoryDatabase();
465
- else if (options.connectors) {
466
- const defaultName = options.default || Object.keys(options.connectors)[0];
467
- const connector = options.connectors[defaultName];
468
- if (connector && createFn) try {
469
- raw = createFn(connector);
470
- } catch {
471
- raw = createInMemoryDatabase();
142
+ await next();
143
+ if (c.res?.headers.get("cache-control")) return;
144
+ const status = c.res?.status ?? 200;
145
+ if (noCacheOnError && (status >= 400 || status === 301)) {
146
+ c.header("Cache-Control", "no-cache, no-store, must-revalidate");
147
+ return;
472
148
  }
473
- else raw = createInMemoryDatabase();
474
- } else raw = createInMemoryDatabase();
475
- const dbName = options.default || "default";
476
- const db = wrapDatabase(raw, dbName);
477
- dbRegistry.set(dbName, db);
478
- if (!defaultDatabase) defaultDatabase = db;
479
- dbHooks.callHook("db:connect", db);
480
- return db;
481
- }
482
- function useDatabase(name) {
483
- if (name) {
484
- const db = dbRegistry.get(name);
485
- if (!db) throw new Error(`Database '${name}' not found. Call defineDatabase() first.`);
486
- return db;
487
- }
488
- if (!defaultDatabase) defaultDatabase = defineDatabase();
489
- return defaultDatabase;
490
- }
491
- async function closeDatabases() {
492
- const dbs = Array.from(dbRegistry.values());
493
- dbRegistry.clear();
494
- defaultDatabase = null;
495
- for (const db of dbs) await db.close();
496
- }
497
- function getDatabaseHooks() {
498
- return dbHooks;
499
- }
500
- function registerDb0Create(fn) {
501
- _global.$db0Create = fn;
502
- }
503
- async function migrateDatabase(db, migrations) {
504
- for (const migration of migrations) await db.exec(migration);
505
- }
506
- async function runMigrations(db, migrations, options = {}) {
507
- const tableName = options.table || "_migrations";
508
- const applied = [];
509
- await db.exec(`CREATE TABLE IF NOT EXISTS ${tableName} (name TEXT PRIMARY KEY, applied_at INTEGER)`);
510
- const { rows } = await db.sql`SELECT name FROM ${rawSql(tableName)}`;
511
- const appliedNames = new Set(rows.map((r) => r.name));
512
- for (const migration of migrations) {
513
- if (appliedNames.has(migration.name)) continue;
514
- await db.exec(migration.up);
515
- await db.sql`INSERT INTO ${rawSql(tableName)} (name, applied_at) VALUES (${migration.name}, ${Date.now()})`;
516
- applied.push(migration.name);
517
- if (options.log) console.log(`[db] Applied migration: ${migration.name}`);
518
- }
519
- return { applied };
520
- }
521
- //#endregion
522
- //#region src/queue.ts
523
- const queueDefinitions = /* @__PURE__ */ new Map();
524
- let globalDriver = null;
525
- const inMemoryQueues = /* @__PURE__ */ new Map();
526
- function generateId$2() {
527
- if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID();
528
- return `msg_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
529
- }
530
- function defineQueue(options, handler) {
531
- if (!options.name) throw new Error("[ubean] Queue must have a name");
532
- const finalHandler = handler || options.handler;
533
- if (!finalHandler) throw new Error(`[ubean] Queue "${options.name}" must have a handler`);
534
- const def = {
535
- name: options.name,
536
- handler: finalHandler,
537
- concurrency: options.concurrency ?? 5,
538
- retries: options.retries ?? 3,
539
- retryDelay: options.retryDelay ?? 1e3,
540
- deadLetterQueue: options.deadLetterQueue
149
+ const matchedRule = rules.find((rule) => {
150
+ if (typeof rule.pattern === "string") return matchGlobPath(path, rule.pattern);
151
+ return rule.pattern.test(path);
152
+ });
153
+ const maxAge = matchedRule?.maxAge ?? defaultMaxAge;
154
+ const swMaxAge = matchedRule?.sMaxAge ?? sMaxAge;
155
+ const isImmutable = matchedRule?.immutable ?? immutable;
156
+ if (maxAge === void 0 && swMaxAge === void 0) return;
157
+ const directives = [];
158
+ if (maxAge === 0) directives.push("no-cache, no-store, must-revalidate");
159
+ else {
160
+ directives.push(isPublic ? "public" : "private");
161
+ if (maxAge !== void 0) directives.push(`max-age=${maxAge}`);
162
+ if (swMaxAge !== void 0) directives.push(`s-maxage=${swMaxAge}`);
163
+ if (staleWhileRevalidate !== void 0) directives.push(`stale-while-revalidate=${staleWhileRevalidate}`);
164
+ if (staleIfError !== void 0) directives.push(`stale-if-error=${staleIfError}`);
165
+ if (isImmutable) directives.push("immutable");
166
+ }
167
+ c.header("Cache-Control", directives.join(", "));
541
168
  };
542
- queueDefinitions.set(options.name, def);
543
- const untypedDef = def;
544
- if (globalDriver?.registerHandler) globalDriver.registerHandler(options.name, finalHandler, untypedDef);
545
- ensureInMemoryQueue(options.name, untypedDef);
546
- return def;
547
- }
548
- function ensureInMemoryQueue(name, options) {
549
- if (!inMemoryQueues.has(name)) inMemoryQueues.set(name, {
550
- messages: [],
551
- processing: /* @__PURE__ */ new Set(),
552
- stats: {
553
- pending: 0,
554
- processing: 0,
555
- completed: 0,
556
- failed: 0
557
- },
558
- options: {
559
- name,
560
- ...options
561
- }
562
- });
563
- return inMemoryQueues.get(name);
564
169
  }
565
- function createMemoryQueueDriver() {
566
- let running = false;
567
- let processTimer = null;
568
- async function processQueue(name) {
569
- const queue = inMemoryQueues.get(name);
570
- if (!queue) return;
571
- const def = queueDefinitions.get(name);
572
- const concurrency = def?.concurrency ?? 5;
573
- const retries = def?.retries ?? 3;
574
- const retryDelay = def?.retryDelay ?? 1e3;
575
- while (queue.processing.size < concurrency && queue.messages.length > 0) {
576
- const msg = queue.messages.shift();
577
- if (!msg) break;
578
- queue.processing.add(msg.id);
579
- queue.stats.processing++;
580
- queue.stats.pending--;
581
- processMessage(name, msg).catch(() => {});
582
- }
583
- async function processMessage(queueName, message) {
584
- const q = inMemoryQueues.get(queueName);
585
- if (!q) return;
586
- const handler = queueDefinitions.get(queueName)?.handler;
587
- if (!handler) {
588
- q.processing.delete(message.id);
589
- q.stats.processing--;
590
- return;
591
- }
170
+ /**
171
+ * 简单的 glob 路径匹配(支持 `*` 单段和 `**` 多段)。
172
+ */
173
+ function matchGlobPath(path, pattern) {
174
+ const regexStr = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
175
+ return new RegExp(`^${regexStr}$`).test(path);
176
+ }
177
+ /**
178
+ * Cloudflare 缓存清除适配器
179
+ *
180
+ * 使用 Cloudflare API Cache-Tag 清除缓存。
181
+ * 需要 zone ID 和 API Token(需 Cache Purge 权限)
182
+ *
183
+ * @example
184
+ * ```typescript
185
+ * const adapter = createCloudflarePurgeAdapter({
186
+ * zoneId: process.env.CF_ZONE_ID!,
187
+ * apiToken: process.env.CF_API_TOKEN!
188
+ * });
189
+ * setGlobalPurgeAdapter(adapter);
190
+ * await purgeKeys(['product:42']);
191
+ * ```
192
+ */
193
+ function createCloudflarePurgeAdapter(options) {
194
+ const endpoint = options.endpoint || "https://api.cloudflare.com/client/v4";
195
+ const { zoneId, apiToken } = options;
196
+ return {
197
+ name: "cloudflare",
198
+ async purgeKeys(keys) {
199
+ if (keys.length === 0) return {
200
+ success: true,
201
+ purged: 0
202
+ };
592
203
  try {
593
- await handler(message);
594
- q.processing.delete(message.id);
595
- q.stats.processing--;
596
- q.stats.completed++;
204
+ const resp = await fetch(`${endpoint}/zones/${zoneId}/purge_cache`, {
205
+ method: "POST",
206
+ headers: {
207
+ Authorization: `Bearer ${apiToken}`,
208
+ "Content-Type": "application/json"
209
+ },
210
+ body: JSON.stringify({ tags: keys })
211
+ });
212
+ const data = await resp.json().catch(() => null);
213
+ if (!resp.ok) return {
214
+ success: false,
215
+ purged: 0,
216
+ error: `Cloudflare API error: ${resp.status} ${resp.statusText}`,
217
+ raw: data
218
+ };
219
+ return {
220
+ success: true,
221
+ purged: keys.length,
222
+ raw: data
223
+ };
597
224
  } catch (err) {
598
- q.processing.delete(message.id);
599
- q.stats.processing--;
600
- message.attempts++;
601
- if (message.attempts >= retries) {
602
- q.stats.failed++;
603
- const dlq = def?.deadLetterQueue;
604
- if (dlq) await driver.send(dlq, message.body, { headers: { "x-error": err instanceof Error ? err.message : String(err) } });
605
- } else {
606
- await new Promise((resolve) => setTimeout(resolve, retryDelay * message.attempts));
607
- q.messages.unshift(message);
608
- q.stats.pending++;
609
- }
610
- }
611
- }
612
- }
613
- const driver = {
614
- async send(queueName, body, options = {}) {
615
- const queue = ensureInMemoryQueue(queueName);
616
- const id = generateId$2();
617
- const message = {
618
- id,
619
- body,
620
- timestamp: Date.now(),
621
- attempts: 0,
622
- headers: options.headers
623
- };
624
- if (options.delay && options.delay > 0) setTimeout(() => {
625
- queue.messages.push(message);
626
- queue.stats.pending++;
627
- if (running) processQueue(queueName);
628
- }, options.delay);
629
- else {
630
- queue.messages.push(message);
631
- queue.stats.pending++;
632
- if (running) processQueue(queueName);
633
- }
634
- return id;
635
- },
636
- async sendBatch(queueName, bodies, options = {}) {
637
- const ids = [];
638
- for (const body of bodies) {
639
- const id = await this.send(queueName, body, options);
640
- ids.push(id);
641
- }
642
- return ids;
643
- },
644
- registerHandler(queueName, handler, options) {
645
- const queue = ensureInMemoryQueue(queueName, options);
646
- queue.handler = handler;
647
- },
648
- async start() {
649
- if (running) return;
650
- running = true;
651
- for (const [name, def] of queueDefinitions) {
652
- const queue = ensureInMemoryQueue(name, def);
653
- queue.handler = def.handler;
654
- }
655
- processTimer = setInterval(() => {
656
- for (const name of inMemoryQueues.keys()) processQueue(name);
657
- }, 100);
658
- for (const name of inMemoryQueues.keys()) processQueue(name);
659
- },
660
- async stop() {
661
- running = false;
662
- if (processTimer) {
663
- clearInterval(processTimer);
664
- processTimer = null;
225
+ return {
226
+ success: false,
227
+ purged: 0,
228
+ error: err instanceof Error ? err.message : String(err)
229
+ };
665
230
  }
666
231
  },
667
- async getQueueDepth(queueName) {
668
- const queue = inMemoryQueues.get(queueName);
669
- if (!queue) return 0;
670
- return queue.messages.length;
671
- },
672
- async deleteMessage(queueName, messageId) {
673
- const queue = inMemoryQueues.get(queueName);
674
- if (!queue) return false;
675
- const idx = queue.messages.findIndex((m) => m.id === messageId);
676
- if (idx >= 0) {
677
- queue.messages.splice(idx, 1);
678
- queue.stats.pending--;
679
- return true;
680
- }
681
- if (queue.processing.has(messageId)) {
682
- queue.processing.delete(messageId);
683
- queue.stats.processing--;
684
- return true;
685
- }
686
- return false;
687
- }
688
- };
689
- return driver;
690
- }
691
- function useQueueDriver() {
692
- if (!globalDriver) globalDriver = createMemoryQueueDriver();
693
- return globalDriver;
694
- }
695
- function setQueueDriver(driver) {
696
- globalDriver = driver;
697
- for (const [name, def] of queueDefinitions) if (driver.registerHandler) driver.registerHandler(name, def.handler, def);
698
- }
699
- async function sendMessage(queueName, body, options) {
700
- return useQueueDriver().send(queueName, body, options);
701
- }
702
- async function sendMessages(queueName, bodies, options) {
703
- return useQueueDriver().sendBatch(queueName, bodies, options);
704
- }
705
- function getQueueDefinitions() {
706
- return Array.from(queueDefinitions.values());
707
- }
708
- function clearQueueDefinitions() {
709
- queueDefinitions.clear();
710
- inMemoryQueues.clear();
711
- }
712
- async function startQueueWorkers() {
713
- const driver = useQueueDriver();
714
- if (driver.start) await driver.start();
715
- }
716
- async function stopQueueWorkers() {
717
- const driver = useQueueDriver();
718
- if (driver.stop) await driver.stop();
719
- }
720
- function getQueueStats(queueName) {
721
- const queue = inMemoryQueues.get(queueName);
722
- if (!queue) return void 0;
723
- return { ...queue.stats };
724
- }
725
- function getAllQueueStats() {
726
- const result = {};
727
- for (const [name, queue] of inMemoryQueues) result[name] = { ...queue.stats };
728
- return result;
729
- }
730
- //#endregion
731
- //#region src/cron.ts
732
- const SCHEDULED_TASKS_KEY = "__ubean_scheduled_tasks__";
733
- const TASK_RUN_COUNT_KEY = "__ubean_task_run_count__";
734
- function getTaskMap() {
735
- if (!globalThis[SCHEDULED_TASKS_KEY]) globalThis[SCHEDULED_TASKS_KEY] = /* @__PURE__ */ new Map();
736
- return globalThis[SCHEDULED_TASKS_KEY];
737
- }
738
- function getTaskRunCount() {
739
- if (!globalThis[TASK_RUN_COUNT_KEY]) globalThis[TASK_RUN_COUNT_KEY] = { value: 0 };
740
- return globalThis[TASK_RUN_COUNT_KEY];
741
- }
742
- function defineScheduled(metaOrName, handler) {
743
- let meta;
744
- let fn;
745
- if (typeof metaOrName === "string") {
746
- meta = {
747
- name: metaOrName,
748
- schedule: "* * * * *"
749
- };
750
- fn = handler || (() => {});
751
- } else {
752
- meta = metaOrName;
753
- fn = handler || (() => {});
754
- }
755
- if (!meta.name) throw new Error("[ubean] Cron task must have a name");
756
- if (!meta.schedule) throw new Error(`[ubean] Cron task "${meta.name}" must have a schedule (cron expression)`);
757
- const task = {
758
- name: meta.name,
759
- schedule: meta.schedule,
760
- handler: fn,
761
- meta: {
762
- description: meta.description,
763
- timezone: meta.timezone,
764
- timeout: meta.timeout,
765
- runOnStart: meta.runOnStart
766
- }
767
- };
768
- getTaskMap().set(meta.name, task);
769
- return task;
770
- }
771
- function getScheduledTasks() {
772
- return Array.from(getTaskMap().values());
773
- }
774
- function clearScheduledTasks() {
775
- getTaskMap().clear();
776
- }
777
- async function runScheduledTask(name) {
778
- const task = getTaskMap().get(name);
779
- if (!task) throw new Error(`[ubean] Cron task "${name}" not found`);
780
- const runCount = ++getTaskRunCount().value;
781
- const start = Date.now();
782
- try {
783
- const ctx = {
784
- name: task.name,
785
- schedule: task.schedule,
786
- timestamp: /* @__PURE__ */ new Date(),
787
- runCount
788
- };
789
- await task.handler(ctx);
790
- return {
791
- ok: true,
792
- duration: Date.now() - start
793
- };
794
- } catch (err) {
795
- return {
796
- ok: false,
797
- duration: Date.now() - start,
798
- error: err instanceof Error ? err : new Error(String(err))
799
- };
800
- }
801
- }
802
- function createCronContext(name, schedule) {
803
- const runCount = ++getTaskRunCount().value;
804
- return {
805
- name,
806
- schedule,
807
- timestamp: /* @__PURE__ */ new Date(),
808
- runCount
809
- };
810
- }
811
- //#endregion
812
- //#region src/cron-scheduler.ts
813
- function parseField(field, min, max) {
814
- const result = /* @__PURE__ */ new Set();
815
- for (const part of field.split(",")) {
816
- const trimmed = part.trim();
817
- if (trimmed === "*") {
818
- for (let i = min; i <= max; i++) result.add(i);
819
- continue;
820
- }
821
- const stepMatch = trimmed.match(/^(.+)\/(\d+)$/);
822
- let base = trimmed;
823
- let step = 1;
824
- if (stepMatch) {
825
- base = stepMatch[1];
826
- step = parseInt(stepMatch[2], 10);
827
- }
828
- if (base === "*") {
829
- for (let i = min; i <= max; i += step) result.add(i);
830
- continue;
831
- }
832
- const rangeMatch = base.match(/^(\d+)-(\d+)$/);
833
- if (rangeMatch) {
834
- const start = parseInt(rangeMatch[1], 10);
835
- const end = parseInt(rangeMatch[2], 10);
836
- for (let i = Math.max(min, start); i <= Math.min(max, end); i += step) result.add(i);
837
- continue;
838
- }
839
- const value = parseInt(base, 10);
840
- if (!isNaN(value) && value >= min && value <= max) result.add(value);
841
- }
842
- return Array.from(result).sort((a, b) => a - b);
843
- }
844
- function parseCron(schedule) {
845
- const parts = schedule.trim().split(/\s+/);
846
- if (parts.length < 5) return null;
847
- const parsed = {
848
- minute: parseField(parts[0], 0, 59),
849
- hour: parseField(parts[1], 0, 23),
850
- dom: parseField(parts[2], 1, 31),
851
- month: parseField(parts[3], 1, 12),
852
- dow: parseField(parts[4], 0, 6)
853
- };
854
- if (parsed.minute.length === 0 || parsed.hour.length === 0 || parsed.dom.length === 0 || parsed.month.length === 0 || parsed.dow.length === 0) return null;
855
- return parsed;
856
- }
857
- function matches(date, parsed) {
858
- return parsed.minute.includes(date.getMinutes()) && parsed.hour.includes(date.getHours()) && parsed.dom.includes(date.getDate()) && parsed.month.includes(date.getMonth() + 1) && parsed.dow.includes(date.getDay());
859
- }
860
- function nextMatch(from, parsed) {
861
- const d = new Date(from);
862
- d.setSeconds(0, 0);
863
- d.setMinutes(d.getMinutes() + 1);
864
- for (let i = 0; i < 366 * 24 * 60; i++) {
865
- if (matches(d, parsed)) return new Date(d);
866
- d.setMinutes(d.getMinutes() + 1);
867
- }
868
- return null;
869
- }
870
- const taskRunCounts = /* @__PURE__ */ new Map();
871
- function getRunCount(name) {
872
- const c = taskRunCounts.get(name) || 0;
873
- taskRunCounts.set(name, c + 1);
874
- return c + 1;
875
- }
876
- function createMemoryCronScheduler(options = {}) {
877
- let timer = null;
878
- let running = false;
879
- const taskTimers = /* @__PURE__ */ new Map();
880
- const runOnStartExecuted = /* @__PURE__ */ new Set();
881
- async function executeTask(task) {
882
- const ctx = {
883
- name: task.name,
884
- schedule: task.schedule,
885
- timestamp: /* @__PURE__ */ new Date(),
886
- runCount: getRunCount(task.name)
887
- };
888
- const start = Date.now();
889
- options.onTaskStart?.(task);
890
- try {
891
- const timeoutMs = task.meta.timeout || options.defaultTimeout || 3e4;
892
- await Promise.race([task.handler(ctx), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error(`Task "${task.name}" timed out after ${timeoutMs}ms`)), timeoutMs))]);
893
- const result = {
894
- ok: true,
895
- duration: Date.now() - start
896
- };
897
- options.onTaskComplete?.(task, result);
898
- return result;
899
- } catch (err) {
900
- const error = err instanceof Error ? err : new Error(String(err));
901
- const result = {
902
- ok: false,
903
- duration: Date.now() - start,
904
- error
232
+ async purgeUrls(urls) {
233
+ if (urls.length === 0) return {
234
+ success: true,
235
+ purged: 0
905
236
  };
906
- options.onTaskError?.(task, error);
907
- options.onTaskComplete?.(task, result);
908
- return result;
909
- }
910
- }
911
- function checkAndRun() {
912
- const tasks = getScheduledTasks();
913
- const now = /* @__PURE__ */ new Date();
914
- for (const task of tasks) {
915
- const parsed = parseCron(task.schedule);
916
- if (!parsed) continue;
917
- if (matches(now, parsed)) {
918
- const timerKey = `${task.name}_${now.getFullYear()}-${now.getMonth()}-${now.getDate()}-${now.getHours()}-${now.getMinutes()}`;
919
- if (taskTimers.has(timerKey)) continue;
920
- if (runOnStartExecuted.has(task.name)) {
921
- runOnStartExecuted.delete(task.name);
922
- taskTimers.set(timerKey, setTimeout(() => {
923
- taskTimers.delete(timerKey);
924
- }, 6e4));
925
- continue;
926
- }
927
- taskTimers.set(timerKey, setTimeout(() => {
928
- taskTimers.delete(timerKey);
929
- }, 6e4));
930
- executeTask(task);
931
- }
932
- }
933
- }
934
- return {
935
- async start() {
936
- if (running) return;
937
- running = true;
938
- const tasks = getScheduledTasks();
939
- for (const task of tasks) if (task.meta.runOnStart) {
940
- runOnStartExecuted.add(task.name);
941
- await executeTask(task);
942
- }
943
- timer = setInterval(checkAndRun, 3e4);
944
- },
945
- async stop() {
946
- running = false;
947
- if (timer) {
948
- clearInterval(timer);
949
- timer = null;
950
- }
951
- for (const t of taskTimers.values()) clearTimeout(t);
952
- taskTimers.clear();
953
- },
954
- async runTask(name) {
955
- const task = getScheduledTasks().find((t) => t.name === name);
956
- if (!task) throw new Error(`[ubean] Cron task "${name}" not found`);
957
- return executeTask(task);
958
- },
959
- isRunning() {
960
- return running;
961
- },
962
- getTasks() {
963
- return getScheduledTasks();
964
- },
965
- getNextRuns() {
966
- return getScheduledTasks().map((task) => {
967
- const parsed = parseCron(task.schedule);
237
+ try {
238
+ const resp = await fetch(`${endpoint}/zones/${zoneId}/purge_cache`, {
239
+ method: "POST",
240
+ headers: {
241
+ Authorization: `Bearer ${apiToken}`,
242
+ "Content-Type": "application/json"
243
+ },
244
+ body: JSON.stringify({ files: urls })
245
+ });
246
+ const data = await resp.json().catch(() => null);
247
+ if (!resp.ok) return {
248
+ success: false,
249
+ purged: 0,
250
+ error: `Cloudflare API error: ${resp.status} ${resp.statusText}`,
251
+ raw: data
252
+ };
968
253
  return {
969
- name: task.name,
970
- nextRun: parsed ? nextMatch(/* @__PURE__ */ new Date(), parsed) : null
254
+ success: true,
255
+ purged: urls.length,
256
+ raw: data
257
+ };
258
+ } catch (err) {
259
+ return {
260
+ success: false,
261
+ purged: 0,
262
+ error: err instanceof Error ? err.message : String(err)
971
263
  };
972
- });
973
- }
974
- };
975
- }
976
- function startCronScheduler(options = {}) {
977
- const scheduler = createMemoryCronScheduler(options);
978
- scheduler.start();
979
- return scheduler;
980
- }
981
- function resetCronRunCounts() {
982
- taskRunCounts.clear();
983
- }
984
- function validateCron(schedule) {
985
- return parseCron(schedule) !== null;
986
- }
987
- //#endregion
988
- //#region src/websocket.ts
989
- const rooms = /* @__PURE__ */ new Map();
990
- const topicSubscribers = /* @__PURE__ */ new Map();
991
- const definitions = /* @__PURE__ */ new Map();
992
- const peerIdSeed = { value: 0 };
993
- var RoomImpl = class {
994
- name;
995
- peers = /* @__PURE__ */ new Set();
996
- constructor(name) {
997
- this.name = name;
998
- }
999
- broadcast(msg, options) {
1000
- for (const peer of this.peers) {
1001
- if (options?.except && peer === options.except) continue;
1002
- if (peer.readyState === 1) try {
1003
- peer.send(msg);
1004
- } catch {}
1005
- }
1006
- }
1007
- add(peer) {
1008
- this.peers.add(peer);
1009
- peer._rooms.add(this.name);
1010
- }
1011
- remove(peer) {
1012
- this.peers.delete(peer);
1013
- peer._rooms.delete(this.name);
1014
- if (this.peers.size === 0) rooms.delete(this.name);
1015
- }
1016
- };
1017
- function generateId$1() {
1018
- peerIdSeed.value++;
1019
- return `peer_${Date.now().toString(36)}_${peerIdSeed.value.toString(36)}`;
1020
- }
1021
- function createPeer(options) {
1022
- const subscriptions = /* @__PURE__ */ new Set();
1023
- const peerRooms = /* @__PURE__ */ new Set();
1024
- let data = void 0;
1025
- const peer = {
1026
- id: options.id,
1027
- url: options.url,
1028
- headers: options.headers,
1029
- get readyState() {
1030
- return 1;
1031
- },
1032
- _subscriptions: subscriptions,
1033
- _rooms: peerRooms,
1034
- _raw: options.raw,
1035
- send: options.send,
1036
- close: options.close,
1037
- publish(topic, msg) {
1038
- const topicPeers = topicSubscribers.get(topic);
1039
- if (topicPeers) {
1040
- for (const p of topicPeers) if (p !== peer && p.readyState === 1) try {
1041
- p.send(msg);
1042
- } catch {}
1043
264
  }
1044
265
  },
1045
- subscribe(topic) {
1046
- subscriptions.add(topic);
1047
- let set = topicSubscribers.get(topic);
1048
- if (!set) {
1049
- set = /* @__PURE__ */ new Set();
1050
- topicSubscribers.set(topic, set);
266
+ async purgeAll() {
267
+ try {
268
+ const resp = await fetch(`${endpoint}/zones/${zoneId}/purge_cache`, {
269
+ method: "POST",
270
+ headers: {
271
+ Authorization: `Bearer ${apiToken}`,
272
+ "Content-Type": "application/json"
273
+ },
274
+ body: JSON.stringify({ purge_everything: true })
275
+ });
276
+ const data = await resp.json().catch(() => null);
277
+ if (!resp.ok) return {
278
+ success: false,
279
+ purged: 0,
280
+ error: `Cloudflare API error: ${resp.status} ${resp.statusText}`,
281
+ raw: data
282
+ };
283
+ return {
284
+ success: true,
285
+ purged: -1,
286
+ raw: data
287
+ };
288
+ } catch (err) {
289
+ return {
290
+ success: false,
291
+ purged: 0,
292
+ error: err instanceof Error ? err.message : String(err)
293
+ };
1051
294
  }
1052
- set.add(peer);
1053
- },
1054
- unsubscribe(topic) {
1055
- subscriptions.delete(topic);
1056
- topicSubscribers.get(topic)?.delete(peer);
1057
- },
1058
- getData() {
1059
- return data;
1060
- },
1061
- setData(value) {
1062
- data = value;
1063
295
  }
1064
296
  };
1065
- return peer;
1066
- }
1067
- function createRoom(name) {
1068
- const existing = rooms.get(name);
1069
- if (existing) return existing;
1070
- const room = new RoomImpl(name);
1071
- rooms.set(name, room);
1072
- return room;
1073
- }
1074
- function defineWebSocket(def) {
1075
- return def;
1076
- }
1077
- function defineRoom(name) {
1078
- return createRoom(name);
1079
- }
1080
- function getRoom(name) {
1081
- return rooms.get(name);
1082
297
  }
1083
- function getRooms() {
1084
- return rooms;
1085
- }
1086
- function broadcast(topic, data) {
1087
- const peers = topicSubscribers.get(topic);
1088
- if (!peers) return;
1089
- for (const peer of peers) if (peer.readyState === 1) try {
1090
- peer.send(data);
1091
- } catch {}
1092
- }
1093
- function registerWebSocket(path, def) {
1094
- definitions.set(path, def);
1095
- }
1096
- function getWebSocketDefinitions() {
1097
- return definitions;
1098
- }
1099
- function handleUpgrade(c, options) {
1100
- const path = new URL(c.req.url).pathname;
1101
- const def = definitions.get(path) || definitions.get("/*");
1102
- const peer = createPeer({
1103
- id: generateId$1(),
1104
- url: c.req.url,
1105
- headers: c.req.raw.headers,
1106
- send: options.send,
1107
- close: options.close,
1108
- raw: options.raw
1109
- });
1110
- if (def?.topics) for (const topic of def.topics) peer.subscribe(topic);
1111
- if (def?.rooms) for (const roomName of def.rooms) createRoom(roomName).add(peer);
1112
- if (def?.hooks?.open) queueMicrotask(() => {
1113
- Promise.resolve(def.hooks.open(peer)).catch(() => {});
1114
- });
298
+ /**
299
+ * Fastly 缓存清除适配器
300
+ *
301
+ * 使用 Fastly API 按 Surrogate-Key 清除缓存。
302
+ * 需要 service ID 和 API key。
303
+ */
304
+ function createFastlyPurgeAdapter(options) {
305
+ const endpoint = options.endpoint || "https://api.fastly.com";
306
+ const { serviceId, apiKey } = options;
1115
307
  return {
1116
- response: new Response(null, {
1117
- status: 200,
1118
- headers: {
1119
- Upgrade: "websocket",
1120
- Connection: "Upgrade"
1121
- }
1122
- }),
1123
- peer
1124
- };
1125
- }
1126
- function handleMessage(peer, message) {
1127
- const path = new URL(peer.url).pathname;
1128
- const def = definitions.get(path) || definitions.get("/*");
1129
- if (def?.hooks?.message) Promise.resolve(def.hooks.message(peer, message)).catch(() => {});
1130
- }
1131
- function handleClose(peer, code = 1e3, reason = "") {
1132
- const path = new URL(peer.url).pathname;
1133
- const def = definitions.get(path) || definitions.get("/*");
1134
- const internal = peer;
1135
- for (const topic of internal._subscriptions) topicSubscribers.get(topic)?.delete(peer);
1136
- for (const roomName of internal._rooms) rooms.get(roomName)?.remove(peer);
1137
- internal._subscriptions.clear();
1138
- if (def?.hooks?.close) Promise.resolve(def.hooks.close(peer, code, reason)).catch(() => {});
1139
- }
1140
- function handleError(peer, error) {
1141
- const path = new URL(peer.url).pathname;
1142
- const def = definitions.get(path) || definitions.get("/*");
1143
- if (def?.hooks?.error) Promise.resolve(def.hooks.error(peer, error)).catch(() => {});
1144
- }
1145
- function createWebSocketMiddleware() {
1146
- return async function wsMiddleware(c, next) {
1147
- if (c.req.header("upgrade")?.toLowerCase() !== "websocket") {
1148
- await next();
1149
- return;
1150
- }
1151
- const path = new URL(c.req.url).pathname;
1152
- if (!definitions.get(path)) {
1153
- await next();
1154
- return;
1155
- }
1156
- return new Response("WebSocket upgrade requires platform-specific handler", { status: 426 });
1157
- };
1158
- }
1159
- function clearWebSocketState() {
1160
- rooms.clear();
1161
- topicSubscribers.clear();
1162
- definitions.clear();
1163
- peerIdSeed.value = 0;
1164
- }
1165
- //#endregion
1166
- //#region src/sse.ts
1167
- const connections = /* @__PURE__ */ new Map();
1168
- let idSeed = 0;
1169
- function generateId() {
1170
- idSeed++;
1171
- return `sse_${Date.now().toString(36)}_${idSeed.toString(36)}`;
1172
- }
1173
- function formatSSEMessage(msg) {
1174
- const lines = [];
1175
- if (msg.comment) for (const line of msg.comment.split("\n")) lines.push(`: ${line}`);
1176
- if (msg.id != null) lines.push(`id: ${msg.id}`);
1177
- if (msg.event) lines.push(`event: ${msg.event}`);
1178
- if (msg.retry != null) lines.push(`retry: ${msg.retry}`);
1179
- if (msg.data != null) {
1180
- const data = typeof msg.data === "string" ? msg.data : JSON.stringify(msg.data);
1181
- for (const line of data.split("\n")) lines.push(`data: ${line}`);
1182
- }
1183
- return `${lines.join("\n")}\n\n`;
1184
- }
1185
- var SSEConnectionImpl = class {
1186
- id;
1187
- writer = null;
1188
- encoder = new TextEncoder();
1189
- _closed = false;
1190
- keepAliveTimer = null;
1191
- onCloseCb;
1192
- constructor(writer, options = {}, onClose) {
1193
- this.id = generateId();
1194
- this.writer = writer;
1195
- this.onCloseCb = onClose;
1196
- if (options.keepAlive !== false) {
1197
- const interval = typeof options.keepAlive === "number" ? options.keepAlive : 3e4;
1198
- this.keepAliveTimer = setInterval(() => {
1199
- if (!this._closed) this.comment("keep-alive");
1200
- }, interval);
1201
- }
1202
- connections.set(this.id, this);
1203
- }
1204
- get closed() {
1205
- return this._closed;
1206
- }
1207
- send(msg) {
1208
- if (this._closed || !this.writer) return;
1209
- const raw = formatSSEMessage(msg);
1210
- this.writer.write(this.encoder.encode(raw)).catch(() => this._cleanup());
1211
- }
1212
- sendData(data, event) {
1213
- this.send({
1214
- data,
1215
- event
1216
- });
1217
- }
1218
- comment(text) {
1219
- if (this._closed || !this.writer) return;
1220
- try {
1221
- const raw = `: ${text}\n\n`;
1222
- this.writer.write(this.encoder.encode(raw)).catch(() => this._cleanup());
1223
- } catch {
1224
- this._cleanup();
1225
- }
1226
- }
1227
- close() {
1228
- this._cleanup();
1229
- }
1230
- _cleanup() {
1231
- if (this._closed) return;
1232
- this._closed = true;
1233
- if (this.keepAliveTimer) {
1234
- clearInterval(this.keepAliveTimer);
1235
- this.keepAliveTimer = null;
1236
- }
1237
- if (this.writer) {
1238
- try {
1239
- this.writer.close();
1240
- } catch {}
1241
- this.writer = null;
1242
- }
1243
- connections.delete(this.id);
1244
- if (this.onCloseCb) {
308
+ name: "fastly",
309
+ async purgeKeys(keys) {
310
+ if (keys.length === 0) return {
311
+ success: true,
312
+ purged: 0
313
+ };
1245
314
  try {
1246
- Promise.resolve(this.onCloseCb(this)).catch(() => {});
1247
- } catch {}
1248
- this.onCloseCb = void 0;
1249
- }
1250
- }
1251
- };
1252
- function createSSEStream(c, handler, options = {}) {
1253
- const { readable, writable } = new TransformStream();
1254
- const writer = writable.getWriter();
1255
- const headers = {
1256
- "Content-Type": "text/event-stream",
1257
- "Cache-Control": "no-cache, no-transform",
1258
- Connection: "keep-alive",
1259
- "X-Accel-Buffering": "no",
1260
- ...options.headers
1261
- };
1262
- const connection = new SSEConnectionImpl(writer, options, handler.onClose);
1263
- if (options.retry != null) connection.send({ retry: options.retry });
1264
- queueMicrotask(() => {
1265
- Promise.resolve(handler.onConnect?.(connection)).catch(() => {});
1266
- });
1267
- return new Response(readable, { headers });
1268
- }
1269
- function defineSSE(handler, options) {
1270
- return (async (c) => {
1271
- return createSSEStream(c, handler, options);
1272
- });
1273
- }
1274
- function getSSEConnections() {
1275
- return connections;
1276
- }
1277
- function broadcastSSE(event, data, filter) {
1278
- for (const conn of connections.values()) if (!conn.closed && (!filter || filter(conn))) conn.sendData(data, event);
1279
- }
1280
- function closeAllSSE() {
1281
- for (const conn of Array.from(connections.values())) conn.close();
1282
- connections.clear();
1283
- }
1284
- function sseHeaders() {
1285
- return {
1286
- "Content-Type": "text/event-stream",
1287
- "Cache-Control": "no-cache, no-transform",
1288
- Connection: "keep-alive",
1289
- "X-Accel-Buffering": "no"
1290
- };
1291
- }
1292
- function clearSSEState() {
1293
- closeAllSSE();
1294
- idSeed = 0;
1295
- }
1296
- //#endregion
1297
- //#region src/storage.ts
1298
- function createMemoryDriver() {
1299
- const store = /* @__PURE__ */ new Map();
1300
- return {
1301
- async getItemRaw(key) {
1302
- return store.get(key);
1303
- },
1304
- async setItemRaw(key, value) {
1305
- store.set(key, value);
1306
- },
1307
- async removeItem(key) {
1308
- store.delete(key);
1309
- },
1310
- async getKeys(base) {
1311
- const prefix = base || "";
1312
- return Array.from(store.keys()).filter((k) => k.startsWith(prefix));
1313
- },
1314
- async clear(base) {
1315
- if (!base) {
1316
- store.clear();
1317
- return;
315
+ const resp = await fetch(`${endpoint}/service/${serviceId}/purge`, {
316
+ method: "POST",
317
+ headers: {
318
+ "Fastly-Key": apiKey,
319
+ "Surrogate-Key": keys.join(" ")
320
+ }
321
+ });
322
+ const data = await resp.json().catch(() => null);
323
+ if (!resp.ok) return {
324
+ success: false,
325
+ purged: 0,
326
+ error: `Fastly API error: ${resp.status} ${resp.statusText}`,
327
+ raw: data
328
+ };
329
+ return {
330
+ success: true,
331
+ purged: keys.length,
332
+ raw: data
333
+ };
334
+ } catch (err) {
335
+ return {
336
+ success: false,
337
+ purged: 0,
338
+ error: err instanceof Error ? err.message : String(err)
339
+ };
1318
340
  }
1319
- for (const key of Array.from(store.keys())) if (key.startsWith(base)) store.delete(key);
1320
- },
1321
- async hasItem(key) {
1322
- return store.has(key);
1323
341
  }
1324
342
  };
1325
343
  }
1326
- function createStorage(options = {}) {
1327
- const mounts = /* @__PURE__ */ new Map();
1328
- const rootDriver = options.driver || createMemoryDriver();
1329
- const rootBase = options.base ? `${options.base.replace(/\/$/, "")}:` : "";
1330
- function resolveKey(key) {
1331
- const fullKey = rootBase + key;
1332
- let bestMatch = "";
1333
- let bestDriver = rootDriver;
1334
- for (const [base, driver] of mounts) if (fullKey.startsWith(`${base}:`) && base.length > bestMatch.length) {
1335
- bestMatch = base;
1336
- bestDriver = driver;
1337
- }
1338
- const relativeKey = bestMatch ? fullKey.slice(bestMatch.length + 1) : fullKey;
1339
- return {
1340
- driver: bestDriver,
1341
- relativeKey
1342
- };
1343
- }
1344
- async function getEntry(key) {
1345
- const { driver, relativeKey } = resolveKey(key);
1346
- const raw = await driver.getItemRaw(relativeKey);
1347
- if (raw === void 0 || raw === null) return null;
1348
- const entry = raw;
1349
- if (entry.expiresAt && Date.now() > entry.expiresAt) {
1350
- await driver.removeItem(relativeKey);
1351
- return null;
1352
- }
1353
- return entry;
1354
- }
344
+ /**
345
+ * Mock purge 适配器(用于测试)
346
+ *
347
+ * 记录所有清除操作,便于断言。
348
+ */
349
+ function createMockPurgeAdapter() {
350
+ const purgedKeys = [];
351
+ const purgedUrls = [];
352
+ let purgedAllCount = 0;
1355
353
  return {
1356
- async get(key) {
1357
- const entry = await getEntry(key);
1358
- return entry ? entry.value : null;
1359
- },
1360
- async set(key, value, ttl) {
1361
- const { driver, relativeKey } = resolveKey(key);
1362
- const entry = {
1363
- value,
1364
- createdAt: Date.now(),
1365
- expiresAt: ttl ? Date.now() + ttl * 1e3 : void 0
354
+ name: "mock",
355
+ purgedKeys,
356
+ purgedUrls,
357
+ get purgedAllCount() {
358
+ return purgedAllCount;
359
+ },
360
+ async purgeKeys(keys) {
361
+ purgedKeys.push([...keys]);
362
+ return {
363
+ success: true,
364
+ purged: keys.length
1366
365
  };
1367
- await driver.setItemRaw(relativeKey, entry);
1368
- },
1369
- async remove(key) {
1370
- const { driver, relativeKey } = resolveKey(key);
1371
- await driver.removeItem(relativeKey);
1372
- },
1373
- async keys(base) {
1374
- const prefix = base || "";
1375
- const rootPrefix = rootBase;
1376
- const results = /* @__PURE__ */ new Set();
1377
- const rootKeys = await rootDriver.getKeys(rootPrefix);
1378
- for (const k of rootKeys) {
1379
- const stripped = k.slice(rootPrefix.length);
1380
- if (!prefix || stripped.startsWith(prefix)) results.add(stripped);
1381
- }
1382
- return Array.from(results);
1383
- },
1384
- async clear(base) {
1385
- const rootPrefix = rootBase + (base || "");
1386
- const rootKeys = await rootDriver.getKeys(rootPrefix);
1387
- for (const k of rootKeys) await rootDriver.removeItem(k);
1388
- },
1389
- async has(key) {
1390
- return await getEntry(key) !== null;
1391
366
  },
1392
- async getMeta(key) {
1393
- const entry = await getEntry(key);
1394
- if (!entry) return null;
367
+ async purgeUrls(urls) {
368
+ purgedUrls.push([...urls]);
1395
369
  return {
1396
- ttl: entry.expiresAt ? Math.max(0, Math.floor((entry.expiresAt - Date.now()) / 1e3)) : void 0,
1397
- createdAt: entry.createdAt,
1398
- expiresAt: entry.expiresAt
370
+ success: true,
371
+ purged: urls.length
1399
372
  };
1400
373
  },
1401
- mount(base, driver) {
1402
- mounts.set(rootBase + base.replace(/\/$/, ""), driver);
1403
- }
1404
- };
1405
- }
1406
- let globalStorage = null;
1407
- function useStorage(storage) {
1408
- if (storage) {
1409
- globalStorage = storage;
1410
- return storage;
1411
- }
1412
- if (!globalStorage) globalStorage = createStorage();
1413
- return globalStorage;
1414
- }
1415
- function clearGlobalStorage() {
1416
- globalStorage = null;
1417
- }
1418
- function createKV(options = {}) {
1419
- const storage = useStorage();
1420
- const prefix = options.prefix || "kv:default";
1421
- const serialize = options.serialize || ((v) => JSON.stringify(v));
1422
- const deserialize = options.deserialize || ((raw) => JSON.parse(raw));
1423
- const fullKey = (key) => `${prefix}:${key}`;
1424
- return {
1425
- async get(key) {
1426
- const raw = await storage.get(fullKey(key));
1427
- if (raw === null) return null;
1428
- return deserialize(raw);
1429
- },
1430
- async set(key, value, ttl) {
1431
- await storage.set(fullKey(key), serialize(value), ttl ?? options.ttl);
1432
- },
1433
- async remove(key) {
1434
- await storage.remove(fullKey(key));
1435
- },
1436
- async keys() {
1437
- return (await storage.keys(prefix)).map((k) => k.slice(prefix.length + 1)).filter((k) => k.length > 0);
1438
- },
1439
- async has(key) {
1440
- return storage.has(fullKey(key));
1441
- },
1442
- async clear() {
1443
- await storage.clear(prefix);
1444
- }
1445
- };
1446
- }
1447
- const namespaces = /* @__PURE__ */ new Map();
1448
- function useKV(name = "default", options) {
1449
- if (namespaces.has(name)) return namespaces.get(name);
1450
- const ns = createKV({
1451
- ...options,
1452
- prefix: `kv:${name}`
1453
- });
1454
- namespaces.set(name, ns);
1455
- return ns;
1456
- }
1457
- //#endregion
1458
- //#region src/observability.ts
1459
- const REQUEST_ID_HEADER = "x-request-id";
1460
- function generateRequestId() {
1461
- if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID();
1462
- return `req_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
1463
- }
1464
- function getRequestId(c) {
1465
- return c.get("requestId");
1466
- }
1467
- function generateSpanId() {
1468
- if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID().replace(/-/g, "").slice(0, 16);
1469
- return Array.from({ length: 16 }, () => Math.floor(Math.random() * 16).toString(16)).join("");
1470
- }
1471
- function generateTraceId() {
1472
- if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID().replace(/-/g, "") + crypto.randomUUID().replace(/-/g, "").slice(0, 16);
1473
- return Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join("");
1474
- }
1475
- function createSpan(options) {
1476
- let ended = false;
1477
- const ctx = {
1478
- traceId: generateTraceId(),
1479
- spanId: generateSpanId()
1480
- };
1481
- if (options.parent) if ("context" in options.parent) {
1482
- ctx.traceId = options.parent.context.traceId;
1483
- ctx.parentSpanId = options.parent.context.spanId;
1484
- } else {
1485
- ctx.traceId = options.parent.traceId;
1486
- ctx.parentSpanId = options.parent.spanId;
1487
- }
1488
- const span = {
1489
- name: options.name,
1490
- context: ctx,
1491
- startTime: Date.now(),
1492
- status: "ok",
1493
- attributes: { ...options.attributes },
1494
- events: [],
1495
- parent: options.parent && "context" in options.parent ? options.parent : void 0,
1496
- end(opts) {
1497
- if (ended) return;
1498
- ended = true;
1499
- span.endTime = Date.now();
1500
- if (opts?.status) span.status = opts.status;
1501
- if (opts?.attributes) Object.assign(span.attributes, opts.attributes);
1502
- if (opts?.error) {
1503
- span.error = opts.error;
1504
- span.status = "error";
1505
- }
1506
- },
1507
- setAttribute(key, value) {
1508
- if (!ended) span.attributes[key] = value;
1509
- },
1510
- setAttributes(attrs) {
1511
- if (!ended) Object.assign(span.attributes, attrs);
1512
- },
1513
- addEvent(name, attributes) {
1514
- if (!ended) span.events.push({
1515
- name,
1516
- timestamp: Date.now(),
1517
- attributes
1518
- });
1519
- },
1520
- isRecording() {
1521
- return !ended;
1522
- },
1523
- duration() {
1524
- if (span.endTime === void 0) return void 0;
1525
- return span.endTime - span.startTime;
1526
- }
1527
- };
1528
- return span;
1529
- }
1530
- function createObservabilityTracer(config = {}) {
1531
- const hooks = createHooks();
1532
- const exporters = [...config.exporters || []];
1533
- const sensitiveKeys = new Set((config.sensitiveKeys || [
1534
- "password",
1535
- "passwd",
1536
- "secret",
1537
- "token",
1538
- "authorization",
1539
- "api_key",
1540
- "apikey",
1541
- "access_token",
1542
- "refresh_token",
1543
- "private_key",
1544
- "credit_card",
1545
- "ssn",
1546
- "auth"
1547
- ]).map((k) => k.toLowerCase()));
1548
- const enabled = config.enabled !== false;
1549
- function addExporter(exporter) {
1550
- exporters.push(exporter);
1551
- }
1552
- function removeExporter(name) {
1553
- const idx = exporters.findIndex((e) => e.name === name);
1554
- if (idx >= 0) exporters.splice(idx, 1);
1555
- }
1556
- function redactAttributes(attrs) {
1557
- const result = {};
1558
- for (const [key, value] of Object.entries(attrs)) if (sensitiveKeys.has(key.toLowerCase())) result[key] = "[REDACTED]";
1559
- else if (typeof value === "string" && value.length > 2048) result[key] = `${value.slice(0, 2048)}...[truncated]`;
1560
- else result[key] = value;
1561
- return result;
1562
- }
1563
- function startTracedSpan(options) {
1564
- if (!enabled) return createSpan(options);
1565
- const span = createSpan({
1566
- ...options,
1567
- attributes: {
1568
- ...config.defaultAttributes,
1569
- ...options.attributes
1570
- }
1571
- });
1572
- hooks.callHook("span:start", span);
1573
- const originalEnd = span.end.bind(span);
1574
- span.end = function(opts) {
1575
- if (opts?.attributes) opts.attributes = redactAttributes(opts.attributes);
1576
- span.attributes = redactAttributes(span.attributes);
1577
- originalEnd(opts);
1578
- hooks.callHook("span:end", span);
1579
- if (span.error) hooks.callHook("span:error", span, span.error);
1580
- for (const exporter of exporters) try {
1581
- exporter.exportSpan(span);
1582
- } catch {}
1583
- };
1584
- return span;
1585
- }
1586
- async function tracedWithSpan(options, fn) {
1587
- const span = startTracedSpan(typeof options === "string" ? { name: options } : options);
1588
- try {
1589
- const result = await fn(span);
1590
- span.end();
1591
- return result;
1592
- } catch (err) {
1593
- span.end({
1594
- error: err instanceof Error ? err : new Error(String(err)),
1595
- status: "error"
1596
- });
1597
- throw err;
1598
- }
1599
- }
1600
- function getActiveSpan() {}
1601
- async function flush() {
1602
- await Promise.all(exporters.map((e) => e.flush?.()));
1603
- }
1604
- async function shutdown() {
1605
- await flush();
1606
- await Promise.all(exporters.map((e) => e.shutdown?.()));
1607
- }
1608
- return {
1609
- hooks,
1610
- startSpan: startTracedSpan,
1611
- withSpan: tracedWithSpan,
1612
- getActiveSpan,
1613
- addExporter,
1614
- removeExporter,
1615
- redactAttributes,
1616
- flush,
1617
- shutdown,
1618
- createSpan
1619
- };
1620
- }
1621
- let globalTracer = null;
1622
- function setGlobalTracer(tracer) {
1623
- globalTracer = tracer;
1624
- }
1625
- function getGlobalTracer() {
1626
- if (!globalTracer) globalTracer = createObservabilityTracer();
1627
- return globalTracer;
1628
- }
1629
- function startSpan(options) {
1630
- return getGlobalTracer().startSpan(typeof options === "string" ? { name: options } : options);
1631
- }
1632
- function withSpan(options, fn) {
1633
- return getGlobalTracer().withSpan(options, fn);
1634
- }
1635
- function createOpenTelemetryExporter(options = {}) {
1636
- const url = options.url || "http://localhost:4318/v1/traces";
1637
- const headers = options.headers || {};
1638
- const serviceName = options.serviceName || "ubean-app";
1639
- const serviceVersion = options.serviceVersion || "0.0.0";
1640
- const fetchImpl = options.fetchImpl || (typeof fetch !== "undefined" ? fetch : void 0);
1641
- const batch = [];
1642
- let batchTimer = null;
1643
- const BATCH_SIZE = 512;
1644
- const FLUSH_INTERVAL = 5e3;
1645
- function spanToOTLP(span) {
1646
- const attributes = [];
1647
- for (const [k, v] of Object.entries(span.attributes)) {
1648
- if (v === void 0 || v === null) continue;
1649
- attributes.push({
1650
- key: k,
1651
- value: typeof v === "string" ? { stringValue: v } : typeof v === "number" ? { intValue: String(Math.floor(v)) } : typeof v === "boolean" ? { boolValue: v } : { stringValue: String(v) }
1652
- });
1653
- }
1654
- const events = span.events.map((e) => ({
1655
- timeUnixNano: e.timestamp * 1e6,
1656
- name: e.name,
1657
- attributes: e.attributes ? Object.entries(e.attributes).filter(([, v]) => v !== void 0 && v !== null).map(([k, v]) => ({
1658
- key: k,
1659
- value: typeof v === "string" ? { stringValue: v } : typeof v === "number" ? { intValue: String(Math.floor(v)) } : { stringValue: String(v) }
1660
- })) : []
1661
- }));
1662
- return {
1663
- traceId: span.context.traceId,
1664
- spanId: span.context.spanId,
1665
- parentSpanId: span.context.parentSpanId || "",
1666
- name: span.name,
1667
- kind: "SPAN_KIND_INTERNAL",
1668
- startTimeUnixNano: span.startTime * 1e6,
1669
- endTimeUnixNano: (span.endTime || Date.now()) * 1e6,
1670
- attributes,
1671
- events,
1672
- status: span.status === "error" ? {
1673
- code: "STATUS_CODE_ERROR",
1674
- message: span.error?.message || ""
1675
- } : { code: "STATUS_CODE_OK" },
1676
- droppedAttributesCount: 0,
1677
- droppedEventsCount: 0
1678
- };
1679
- }
1680
- async function exportBatch() {
1681
- if (batch.length === 0 || !fetchImpl) return;
1682
- const spans = batch.splice(0, BATCH_SIZE);
1683
- try {
1684
- await fetchImpl(url, {
1685
- method: "POST",
1686
- headers: {
1687
- "Content-Type": "application/json",
1688
- ...headers
1689
- },
1690
- body: JSON.stringify({ resourceSpans: [{
1691
- resource: { attributes: [{
1692
- key: "service.name",
1693
- value: { stringValue: serviceName }
1694
- }, {
1695
- key: "service.version",
1696
- value: { stringValue: serviceVersion }
1697
- }] },
1698
- scopeSpans: [{
1699
- scope: { name: "ubean" },
1700
- spans
1701
- }]
1702
- }] })
1703
- });
1704
- } catch {}
1705
- }
1706
- function scheduleFlush() {
1707
- if (batchTimer) return;
1708
- batchTimer = setTimeout(() => {
1709
- batchTimer = null;
1710
- exportBatch();
1711
- }, FLUSH_INTERVAL);
1712
- }
1713
- return {
1714
- name: "opentelemetry",
1715
- exportSpan(span) {
1716
- batch.push(spanToOTLP(span));
1717
- if (batch.length >= BATCH_SIZE) exportBatch();
1718
- else scheduleFlush();
1719
- },
1720
- async flush() {
1721
- while (batch.length > 0) await exportBatch();
1722
- },
1723
- async shutdown() {
1724
- if (batchTimer) {
1725
- clearTimeout(batchTimer);
1726
- batchTimer = null;
1727
- }
1728
- await this.flush?.();
1729
- }
1730
- };
1731
- }
1732
- function createConsoleExporter(options = {}) {
1733
- const slowThreshold = options.slowThreshold ?? 1e3;
1734
- return {
1735
- name: "console",
1736
- exportSpan(span) {
1737
- const duration = span.duration() ?? 0;
1738
- const status = span.status === "error" ? "✗" : span.status === "cancelled" ? "○" : "✓";
1739
- const slow = duration >= slowThreshold ? " (slow)" : "";
1740
- const errMsg = span.error ? ` — ${span.error.message}` : "";
1741
- if (!(typeof globalThis.console !== "undefined")) return;
1742
- const msg = `${status} [${duration}ms] ${span.name}${slow}${errMsg}`;
1743
- if (span.status === "error") console.error(msg);
1744
- else if (slow) console.warn(msg);
1745
- else console.debug(msg);
1746
- }
1747
- };
1748
- }
1749
- function createTracingMiddleware(options = {}) {
1750
- const tracer = options.tracer || getGlobalTracer();
1751
- const headerFilter = options.headerFilter || (() => false);
1752
- const includeHeaders = options.includeHeaders ?? false;
1753
- return async function tracingMiddleware(c, next) {
1754
- const method = c.req.method || "GET";
1755
- const path = c.req.path || "/";
1756
- const requestId = getRequestId(c);
1757
- const span = tracer.startSpan({
1758
- name: `${method} ${path}`,
1759
- attributes: {
1760
- "http.method": method,
1761
- "http.url": path,
1762
- "http.request_id": requestId
1763
- }
1764
- });
1765
- c.set("span", span);
1766
- if (includeHeaders) {
1767
- const headers = {};
1768
- for (const [k, v] of Object.entries(c.req.header?.() || {})) if (!headerFilter(k)) headers[k] = v;
1769
- span.setAttributes(tracer.redactAttributes(headers));
1770
- }
1771
- const start = Date.now();
1772
- try {
1773
- await next();
1774
- const statusCode = c.res?.status || 200;
1775
- span.setAttribute("http.status_code", statusCode);
1776
- if (statusCode >= 500) span.end({ status: "error" });
1777
- else span.end({ status: statusCode >= 400 ? "error" : "ok" });
1778
- } catch (err) {
1779
- const duration = Date.now() - start;
1780
- span.setAttribute("error.duration_ms", duration);
1781
- span.end({
1782
- error: err instanceof Error ? err : new Error(String(err)),
1783
- status: "error"
1784
- });
1785
- throw err;
1786
- }
1787
- };
1788
- }
1789
- function getSpan(c) {
1790
- return c.get("span");
1791
- }
1792
- //#endregion
1793
- //#region src/static.ts
1794
- const MIME_TYPES = {
1795
- ".html": "text/html; charset=utf-8",
1796
- ".htm": "text/html; charset=utf-8",
1797
- ".css": "text/css; charset=utf-8",
1798
- ".js": "application/javascript; charset=utf-8",
1799
- ".mjs": "application/javascript; charset=utf-8",
1800
- ".json": "application/json; charset=utf-8",
1801
- ".svg": "image/svg+xml",
1802
- ".png": "image/png",
1803
- ".jpg": "image/jpeg",
1804
- ".jpeg": "image/jpeg",
1805
- ".gif": "image/gif",
1806
- ".webp": "image/webp",
1807
- ".ico": "image/x-icon",
1808
- ".avif": "image/avif",
1809
- ".woff": "font/woff",
1810
- ".woff2": "font/woff2",
1811
- ".ttf": "font/ttf",
1812
- ".otf": "font/otf",
1813
- ".eot": "application/vnd.ms-fontobject",
1814
- ".txt": "text/plain; charset=utf-8",
1815
- ".xml": "application/xml; charset=utf-8",
1816
- ".pdf": "application/pdf",
1817
- ".zip": "application/zip",
1818
- ".gz": "application/gzip",
1819
- ".mp3": "audio/mpeg",
1820
- ".mp4": "video/mp4",
1821
- ".webm": "video/webm",
1822
- ".wasm": "application/wasm",
1823
- ".map": "application/json; charset=utf-8"
1824
- };
1825
- function getMimeType(filePath) {
1826
- const ext = extname(filePath).toLowerCase();
1827
- return MIME_TYPES[ext] || "application/octet-stream";
1828
- }
1829
- function serveStatic(options) {
1830
- const { publicDir, indexFiles = ["index.html"], maxAge = 3600 } = options;
1831
- return async (c, next) => {
1832
- const path = decodeURIComponent(c.req.path);
1833
- if (path.startsWith("/_") || path.startsWith("/api/")) return next();
1834
- let filePath = join(publicDir, path);
1835
- try {
1836
- if (!existsSync(filePath)) {
1837
- if (path.endsWith("/")) for (const indexFile of indexFiles) {
1838
- const indexPath = join(filePath, indexFile);
1839
- if (existsSync(indexPath) && statSync(indexPath).isFile()) {
1840
- filePath = indexPath;
1841
- break;
1842
- }
1843
- }
1844
- if (!existsSync(filePath) || !statSync(filePath).isFile()) return next();
1845
- }
1846
- if (statSync(filePath).isDirectory()) for (const indexFile of indexFiles) {
1847
- const indexPath = join(filePath, indexFile);
1848
- if (existsSync(indexPath) && statSync(indexPath).isFile()) {
1849
- filePath = indexPath;
1850
- break;
1851
- }
1852
- }
1853
- if (!existsSync(filePath) || statSync(filePath).isDirectory()) return next();
1854
- const finalStat = statSync(filePath);
1855
- const mimeType = getMimeType(filePath);
1856
- const body = readFileSync(filePath);
1857
- const headers = {
1858
- "Content-Type": mimeType,
1859
- "Content-Length": String(body.length),
1860
- "Cache-Control": `public, max-age=${maxAge}`,
1861
- "X-Content-Type-Options": "nosniff"
1862
- };
1863
- const ifNoneMatch = c.req.header("If-None-Match");
1864
- const etag = `"${finalStat.size.toString(16)}-${finalStat.mtimeMs.toString(16)}"`;
1865
- if (ifNoneMatch === etag) return new Response(null, {
1866
- status: 304,
1867
- headers
1868
- });
1869
- headers.ETag = etag;
1870
- return new Response(body, {
1871
- status: 200,
1872
- headers
1873
- });
1874
- } catch {
1875
- return next();
1876
- }
1877
- };
1878
- }
1879
- //#endregion
1880
- //#region src/cors.ts
1881
- const DEFAULT_METHODS = [
1882
- "GET",
1883
- "HEAD",
1884
- "PUT",
1885
- "POST",
1886
- "DELETE",
1887
- "PATCH",
1888
- "OPTIONS"
1889
- ];
1890
- const DEFAULT_HEADERS = [
1891
- "Content-Type",
1892
- "Authorization",
1893
- "X-Requested-With",
1894
- "Accept",
1895
- "Origin"
1896
- ];
1897
- function isPreflight(c) {
1898
- return c.req.method === "OPTIONS" && !!c.req.header("origin") && !!c.req.header("access-control-request-method");
1899
- }
1900
- function configureOrigin(c, option, requestOrigin) {
1901
- if (option === true || option === void 0 || option === "*") return "*";
1902
- if (option === false) return null;
1903
- if (typeof option === "string") return option;
1904
- if (Array.isArray(option)) {
1905
- if (option.includes(requestOrigin) || option.includes("*")) return requestOrigin;
1906
- return null;
1907
- }
1908
- if (typeof option === "function") return null;
1909
- return null;
1910
- }
1911
- function createCorsMiddleware(options = {}) {
1912
- const { origin = "*", allowMethods = DEFAULT_METHODS, allowHeaders, exposeHeaders = [], credentials = false, maxAge, preflightContinue = false } = options;
1913
- const resolvedAllowHeaders = allowHeaders?.length ? allowHeaders : DEFAULT_HEADERS;
1914
- return async function corsMiddleware(c, next) {
1915
- const requestOrigin = c.req.header("origin") || "";
1916
- let originValue = null;
1917
- if (typeof origin === "function") {
1918
- const result = await origin(requestOrigin, c);
1919
- if (result === true || result === void 0) originValue = requestOrigin;
1920
- else if (result === false) originValue = null;
1921
- else if (typeof result === "string") originValue = result;
1922
- } else originValue = configureOrigin(c, origin, requestOrigin);
1923
- if (originValue) {
1924
- c.header("Access-Control-Allow-Origin", originValue);
1925
- if (originValue !== "*" || credentials) c.header("Vary", "Origin");
1926
- }
1927
- if (credentials) c.header("Access-Control-Allow-Credentials", "true");
1928
- if (exposeHeaders.length > 0) c.header("Access-Control-Expose-Headers", exposeHeaders.join(", "));
1929
- if (isPreflight(c)) {
1930
- const preflightHeaders = {};
1931
- if (originValue) preflightHeaders["Access-Control-Allow-Origin"] = originValue;
1932
- if (credentials) preflightHeaders["Access-Control-Allow-Credentials"] = "true";
1933
- preflightHeaders["Access-Control-Allow-Methods"] = allowMethods.join(", ");
1934
- preflightHeaders["Access-Control-Allow-Headers"] = resolvedAllowHeaders.join(", ");
1935
- if (maxAge != null) preflightHeaders["Access-Control-Max-Age"] = String(maxAge);
1936
- if (originValue && originValue !== "*") preflightHeaders["Vary"] = "Origin";
1937
- if (!preflightContinue) return new Response(null, {
1938
- status: 204,
1939
- headers: preflightHeaders
1940
- });
1941
- }
1942
- await next();
1943
- };
1944
- }
1945
- function defineCors(options) {
1946
- return createCorsMiddleware(options);
1947
- }
1948
- //#endregion
1949
- //#region src/rate-limit.ts
1950
- var MemoryRateLimitStore = class {
1951
- store = /* @__PURE__ */ new Map();
1952
- timer = null;
1953
- constructor() {
1954
- this.timer = setInterval(() => this._cleanup(), 6e4);
1955
- }
1956
- _cleanup() {
1957
- const now = Date.now();
1958
- for (const [key, entry] of this.store.entries()) if (entry.expireAt <= now) this.store.delete(key);
1959
- }
1960
- async get(key) {
1961
- const entry = this.store.get(key);
1962
- if (!entry || entry.expireAt <= Date.now()) {
1963
- if (entry) this.store.delete(key);
1964
- return;
1965
- }
1966
- return {
1967
- count: entry.count,
1968
- resetAt: entry.resetAt
1969
- };
1970
- }
1971
- async set(key, entry, ttlMs) {
1972
- this.store.set(key, {
1973
- ...entry,
1974
- expireAt: Date.now() + ttlMs
1975
- });
1976
- }
1977
- async increment(key, windowMs) {
1978
- const now = Date.now();
1979
- const existing = this.store.get(key);
1980
- if (!existing || existing.resetAt <= now) {
1981
- const entry = {
1982
- count: 1,
1983
- resetAt: now + windowMs,
1984
- expireAt: now + windowMs
1985
- };
1986
- this.store.set(key, entry);
374
+ async purgeAll() {
375
+ purgedAllCount++;
1987
376
  return {
1988
- count: 1,
1989
- resetAt: entry.resetAt
377
+ success: true,
378
+ purged: -1
1990
379
  };
380
+ },
381
+ reset() {
382
+ purgedKeys.length = 0;
383
+ purgedUrls.length = 0;
384
+ purgedAllCount = 0;
1991
385
  }
1992
- existing.count++;
1993
- return {
1994
- count: existing.count,
1995
- resetAt: existing.resetAt
1996
- };
1997
- }
1998
- async reset(key) {
1999
- this.store.delete(key);
2000
- }
2001
- destroy() {
2002
- if (this.timer) {
2003
- clearInterval(this.timer);
2004
- this.timer = null;
2005
- }
2006
- this.store.clear();
2007
- }
2008
- };
2009
- function defaultKeyGenerator(c) {
2010
- const forwarded = c.req.header("x-forwarded-for");
2011
- if (forwarded) return forwarded.split(",")[0].trim();
2012
- const realIp = c.req.header("x-real-ip");
2013
- if (realIp) return realIp;
2014
- return c.req.raw.headers.get("cf-connecting-ip") || "unknown";
2015
- }
2016
- function defaultHandler(c, info) {
2017
- c.header("Retry-After", String(Math.ceil(info.retryAfter / 1e3)));
2018
- return c.json({
2019
- error: "Too Many Requests",
2020
- message: `Rate limit exceeded. Try again in ${Math.ceil(info.retryAfter / 1e3)} seconds.`,
2021
- limit: info.limit,
2022
- remaining: info.remaining,
2023
- reset: info.reset
2024
- }, 429);
2025
- }
2026
- function createRateLimitMiddleware(options = {}) {
2027
- const { maxRequests = 100, windowMs = 6e4, keyGenerator = defaultKeyGenerator, handler = defaultHandler, skip, standardHeaders = true, legacyHeaders = false, store: customStore } = options;
2028
- const store = customStore || new MemoryRateLimitStore();
2029
- return async function rateLimitMiddleware(c, next) {
2030
- if (skip && await skip(c)) {
2031
- await next();
2032
- return;
2033
- }
2034
- const key = keyGenerator(c);
2035
- const result = await store.increment(key, windowMs);
2036
- const now = Date.now();
2037
- const remaining = Math.max(0, maxRequests - result.count);
2038
- const reset = Math.ceil(result.resetAt / 1e3);
2039
- const retryAfter = Math.max(0, result.resetAt - now);
2040
- if (standardHeaders) {
2041
- c.header("RateLimit-Limit", String(maxRequests));
2042
- c.header("RateLimit-Remaining", String(remaining));
2043
- c.header("RateLimit-Reset", String(reset));
2044
- }
2045
- if (legacyHeaders) {
2046
- c.header("X-RateLimit-Limit", String(maxRequests));
2047
- c.header("X-RateLimit-Remaining", String(remaining));
2048
- c.header("X-RateLimit-Reset", String(reset));
2049
- }
2050
- if (result.count > maxRequests) return handler(c, {
2051
- limit: maxRequests,
2052
- remaining: 0,
2053
- reset,
2054
- retryAfter
2055
- });
2056
- await next();
2057
386
  };
2058
387
  }
2059
- function defineRateLimit(options) {
2060
- return createRateLimitMiddleware(options);
2061
- }
2062
- function createMemoryRateLimitStore() {
2063
- return new MemoryRateLimitStore();
388
+ /**
389
+ * 清除全局状态(主要用于测试隔离)
390
+ */
391
+ function _resetCdnCache() {
392
+ globalPurgeAdapter = null;
2064
393
  }
2065
394
  //#endregion
2066
- export { REQUEST_ID_HEADER, broadcast, broadcastSSE, cachedEventHandler, clearCacheStore, clearGlobalStorage, clearQueueDefinitions, clearSSEState, clearScheduledTasks, clearWebSocketState, closeAllSSE, closeDatabases, createCacheMiddleware, createConsoleExporter, createCorsMiddleware, createCronContext, createKV, createMemoryCronScheduler, createMemoryDriver, createMemoryQueueDriver, createMemoryRateLimitStore, createMemoryStore, createObservabilityTracer, createOpenTelemetryExporter, createRateLimitMiddleware, createRoom, createSSEStream, createSpan, createStorage, createTracingMiddleware, createWebSocketMiddleware, defineCors, defineDatabase, defineQueue, defineRateLimit, defineRoom, defineSSE, defineScheduled, defineWebSocket, formatSSEMessage, generateRequestId, getAllQueueStats, getDatabaseHooks, getGlobalTracer, getQueueDefinitions, getQueueStats, getRequestId, getRoom, getRooms, getSSEConnections, getScheduledTasks, getSpan, getWebSocketDefinitions, handleClose, handleError, handleMessage, handleUpgrade, invalidateRouteCache, migrateDatabase, parseCron, rawSql as raw, rawSql, rawSql as sqlRaw, registerDb0Create, registerWebSocket, resetCronRunCounts, resolveRouteCacheRules, runMigrations, runScheduledTask, sendMessage, sendMessages, serveStatic, setGlobalTracer, setQueueDriver, sseHeaders, startCronScheduler, startQueueWorkers, startSpan, stopQueueWorkers, useCacheStore, useDatabase, useKV, useQueueDriver, useStorage, validateCron, withSpan };
395
+ export { REQUEST_ID_HEADER, _resetCdnCache, after, broadcast, broadcastSSE, cacheLife, cacheTag, cachedEventHandler, clearAnalyticsProviders, clearCacheStore, clearComponentCache, clearComponentCacheStore, clearEmailProviders, clearFeatureFlags, clearFetchDataCache, clearGlobalStorage, clearQueueDefinitions, clearRevalidationRegistry, clearSSEState, clearScheduledTasks, clearSentEmails, clearWebSocketState, closeAllSSE, closeDatabases, createAfterMiddleware, createAnalyticsMiddleware, createCacheControlMiddleware, createCacheMiddleware, createCloudflarePurgeAdapter, createComponentMemoryStore, createConsoleExporter, createCorsMiddleware, createCronContext, createCsrfMiddleware, createDataCacheMiddleware, createDraftModeMiddleware, createEmailTransport, createFastlyPurgeAdapter, createFeatureFlagsMiddleware, createFetchMemoizationMiddleware, createKV, createLogAnalyticsProvider, createMemoizedFetch, createMemoryAnalyticsProvider, createMemoryCronScheduler, createMemoryDriver, createMemoryFeatureFlagStore, createMemoryQueueDriver, createMemoryRateLimitStore, createMemoryStore, createMockAnalyticsProvider, createMockPurgeAdapter, createObservabilityTracer, createOpenTelemetryExporter, createRateLimitMiddleware, createRoom, createSSEStream, createSecurityHeadersMiddleware, createSessionMiddleware, createSingleFlightMiddleware, createSpan, createStorage, createStorageSessionStore, createTracingMiddleware, createWebSocketMiddleware, defineAnalytics, defineAnalyticsProvider, defineCachedFunction, defineCors, defineCsrf, defineDatabase, defineDraftMode, defineEmailProvider, defineExperiment, defineFeatureFlag, defineQueue, defineRateLimit, defineRevalidation, defineRoom, defineSSE, defineScheduled, defineSecurityHeaders, defineSessionStore, defineSingleFlight, defineSurrogateKeys, defineWebSocket, disableDraftMode, enableDraftMode, evaluateFlag, evaluateFlagFromContext, evaluateFlagWithReason, extractAnalyticsContext, extractFlagContext, flushAfterCallbacks, formatSSEMessage, generateCsrfToken, generateRequestId, getAfterCallbackCount, getAllQueueStats, getAnalyticsProvider, getDataCacheSize, getDatabaseHooks, getEmailProvider, getEmailProviderNames, getEmailTransport, getGlobalAnalyticsProvider, getGlobalFeatureFlagStore, getGlobalPurgeAdapter, getGlobalTracer, getInvalidatedKeys, getQueueDefinitions, getQueueStats, getRequestId, getRevalidationEntries, getRoom, getRooms, getSSEConnections, getScheduledTasks, getSentEmails, getSpan, getSurrogateKeys, getVariant, getVariantAssignment, getVariantFromContext, getWebSocketDefinitions, handleClose, handleError, handleMessage, handleUpgrade, invalidate, invalidateKey, invalidateRouteCache, isDraftMode, listAnalyticsProviders, listExperimentNames, listFlagNames, migrateDatabase, parseCron, purgeKeys, purgeUrls, rawSql as raw, rawSql, rawSql as sqlRaw, registerAnalyticsProvider, registerDb0Create, registerWebSocket, removeExperiment, removeFeatureFlag, renderEmailTemplate, resetCronRunCounts, resolveRouteCacheRules, revalidateDataCachePath, revalidateDataCacheTag, revalidatePath, revalidateTag, revalidateTags, runMigrations, runRevalidation, runScheduledTask, sendEmail, sendMessage, sendMessages, serializeCsp, serveStatic, setDefaultEmailProvider, setGlobalAnalyticsProvider, setGlobalFeatureFlagStore, setGlobalPurgeAdapter, setGlobalTracer, setQueueDriver, sseHeaders, startCronScheduler, startQueueWorkers, startSpan, stopQueueWorkers, trackEvent, trackPageView, trackRaw, unregisterAnalyticsProvider, unregisterRevalidation, useAnalytics, useCacheStore, useComponentCacheStore, useDatabase, useDraftMode, useEmailProvider, useExperiments, useFlagContext, useFlags, useKV, useQueueDriver, useSession, useStorage, validateCron, withSpan, wrapWithCache };