@ubean/server 0.1.12 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analytics-entry.d.ts +2 -0
- package/dist/analytics-entry.js +2 -0
- package/dist/cache-C84ix1Vq.js +173 -0
- package/dist/cache-b-MZlyv0.d.ts +48 -0
- package/dist/cache-directive-C1Nekkza.js +304 -0
- package/dist/cache-directive-CAxJAQyE.d.ts +175 -0
- package/dist/cache-directive.d.ts +2 -0
- package/dist/cache-directive.js +2 -0
- package/dist/cache-entry.d.ts +3 -0
- package/dist/cache-entry.js +3 -0
- package/dist/cron-entry.d.ts +2 -0
- package/dist/cron-entry.js +2 -0
- package/dist/cron-scheduler-BF33PPn4.d.ts +77 -0
- package/dist/cron-scheduler-BVuXv7nn.js +258 -0
- package/dist/database-CfpFznl-.d.ts +67 -0
- package/dist/database-DNrY44SQ.js +352 -0
- package/dist/database.d.ts +2 -0
- package/dist/database.js +2 -0
- package/dist/email-BjfRiR9b.js +354 -0
- package/dist/email-BvpEuNn_.d.ts +226 -0
- package/dist/email.d.ts +2 -0
- package/dist/email.js +2 -0
- package/dist/feature-flags-CdLwsMD2.js +657 -0
- package/dist/feature-flags-DWkS6p0D.d.ts +386 -0
- package/dist/fetch-memo-rbkxxnW4.js +338 -0
- package/dist/index.d.ts +183 -488
- package/dist/index.js +352 -2023
- package/dist/middleware.d.ts +2 -0
- package/dist/middleware.js +3 -0
- package/dist/observability-Cio6Qq1H.js +339 -0
- package/dist/observability-DUNUEjj3.d.ts +70 -0
- package/dist/observability.d.ts +2 -0
- package/dist/observability.js +2 -0
- package/dist/queue-Bwzi3mhK.js +210 -0
- package/dist/queue-GOfTAWlz.d.ts +55 -0
- package/dist/queue.d.ts +2 -0
- package/dist/queue.js +2 -0
- package/dist/realtime.d.ts +2 -0
- package/dist/realtime.js +2 -0
- package/dist/security.d.ts +2 -0
- package/dist/security.js +2 -0
- package/dist/sessions-BLqFFQTL.d.ts +217 -0
- package/dist/sessions-BsBsyFAG.js +450 -0
- package/dist/single-flight-BJyhDLdU.d.ts +422 -0
- package/dist/single-flight-mJ4ZKbx1.js +715 -0
- package/dist/sse-Ct72zhic.d.ts +95 -0
- package/dist/sse-a6Ky9Vcl.js +310 -0
- package/dist/static-DPHaovQe.js +90 -0
- package/dist/static-K2dRvjpS.d.ts +11 -0
- package/dist/static.d.ts +2 -0
- package/dist/static.js +2 -0
- package/dist/storage-BZLMaqHr.js +162 -0
- package/dist/storage-QdlPtPtR.d.ts +48 -0
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +2 -0
- package/package.json +68 -6
package/dist/index.js
CHANGED
|
@@ -1,2066 +1,395 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
491
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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
|
-
|
|
970
|
-
|
|
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
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
}
|
|
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
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
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
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
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
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
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
|
|
1393
|
-
|
|
1394
|
-
if (!entry) return null;
|
|
367
|
+
async purgeUrls(urls) {
|
|
368
|
+
purgedUrls.push([...urls]);
|
|
1395
369
|
return {
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
expiresAt: entry.expiresAt
|
|
370
|
+
success: true,
|
|
371
|
+
purged: urls.length
|
|
1399
372
|
};
|
|
1400
373
|
},
|
|
1401
|
-
|
|
1402
|
-
|
|
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
|
-
|
|
1989
|
-
|
|
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
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
function
|
|
2063
|
-
|
|
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 };
|