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