@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.
Files changed (56) hide show
  1. package/dist/analytics-entry.d.ts +2 -0
  2. package/dist/analytics-entry.js +2 -0
  3. package/dist/cache-C84ix1Vq.js +173 -0
  4. package/dist/cache-b-MZlyv0.d.ts +48 -0
  5. package/dist/cache-directive-C1Nekkza.js +304 -0
  6. package/dist/cache-directive-CAxJAQyE.d.ts +175 -0
  7. package/dist/cache-directive.d.ts +2 -0
  8. package/dist/cache-directive.js +2 -0
  9. package/dist/cache-entry.d.ts +3 -0
  10. package/dist/cache-entry.js +3 -0
  11. package/dist/cron-entry.d.ts +2 -0
  12. package/dist/cron-entry.js +2 -0
  13. package/dist/cron-scheduler-BF33PPn4.d.ts +77 -0
  14. package/dist/cron-scheduler-BVuXv7nn.js +258 -0
  15. package/dist/database-CfpFznl-.d.ts +67 -0
  16. package/dist/database-DNrY44SQ.js +352 -0
  17. package/dist/database.d.ts +2 -0
  18. package/dist/database.js +2 -0
  19. package/dist/email-BjfRiR9b.js +354 -0
  20. package/dist/email-BvpEuNn_.d.ts +226 -0
  21. package/dist/email.d.ts +2 -0
  22. package/dist/email.js +2 -0
  23. package/dist/feature-flags-CdLwsMD2.js +657 -0
  24. package/dist/feature-flags-DWkS6p0D.d.ts +386 -0
  25. package/dist/fetch-memo-rbkxxnW4.js +338 -0
  26. package/dist/index.d.ts +183 -488
  27. package/dist/index.js +352 -2023
  28. package/dist/middleware.d.ts +2 -0
  29. package/dist/middleware.js +3 -0
  30. package/dist/observability-Cio6Qq1H.js +339 -0
  31. package/dist/observability-DUNUEjj3.d.ts +70 -0
  32. package/dist/observability.d.ts +2 -0
  33. package/dist/observability.js +2 -0
  34. package/dist/queue-Bwzi3mhK.js +210 -0
  35. package/dist/queue-GOfTAWlz.d.ts +55 -0
  36. package/dist/queue.d.ts +2 -0
  37. package/dist/queue.js +2 -0
  38. package/dist/realtime.d.ts +2 -0
  39. package/dist/realtime.js +2 -0
  40. package/dist/security.d.ts +2 -0
  41. package/dist/security.js +2 -0
  42. package/dist/sessions-BLqFFQTL.d.ts +217 -0
  43. package/dist/sessions-BsBsyFAG.js +450 -0
  44. package/dist/single-flight-BJyhDLdU.d.ts +422 -0
  45. package/dist/single-flight-mJ4ZKbx1.js +715 -0
  46. package/dist/sse-Ct72zhic.d.ts +95 -0
  47. package/dist/sse-a6Ky9Vcl.js +310 -0
  48. package/dist/static-DPHaovQe.js +90 -0
  49. package/dist/static-K2dRvjpS.d.ts +11 -0
  50. package/dist/static.d.ts +2 -0
  51. package/dist/static.js +2 -0
  52. package/dist/storage-BZLMaqHr.js +162 -0
  53. package/dist/storage-QdlPtPtR.d.ts +48 -0
  54. package/dist/storage.d.ts +2 -0
  55. package/dist/storage.js +2 -0
  56. package/package.json +68 -6
@@ -0,0 +1,715 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ //#region src/cors.ts
3
+ const DEFAULT_METHODS = [
4
+ "GET",
5
+ "HEAD",
6
+ "PUT",
7
+ "POST",
8
+ "DELETE",
9
+ "PATCH",
10
+ "OPTIONS"
11
+ ];
12
+ const DEFAULT_HEADERS = [
13
+ "Content-Type",
14
+ "Authorization",
15
+ "X-Requested-With",
16
+ "Accept",
17
+ "Origin"
18
+ ];
19
+ function isPreflight(c) {
20
+ return c.req.method === "OPTIONS" && !!c.req.header("origin") && !!c.req.header("access-control-request-method");
21
+ }
22
+ function configureOrigin(c, option, requestOrigin) {
23
+ if (option === true || option === void 0 || option === "*") return "*";
24
+ if (option === false) return null;
25
+ if (typeof option === "string") return option;
26
+ if (Array.isArray(option)) {
27
+ if (option.includes(requestOrigin) || option.includes("*")) return requestOrigin;
28
+ return null;
29
+ }
30
+ if (typeof option === "function") return null;
31
+ return null;
32
+ }
33
+ function createCorsMiddleware(options = {}) {
34
+ const { origin = "*", allowMethods = DEFAULT_METHODS, allowHeaders, exposeHeaders = [], credentials = false, maxAge, preflightContinue = false } = options;
35
+ const resolvedAllowHeaders = allowHeaders?.length ? allowHeaders : DEFAULT_HEADERS;
36
+ return async function corsMiddleware(c, next) {
37
+ const requestOrigin = c.req.header("origin") || "";
38
+ let originValue = null;
39
+ if (typeof origin === "function") {
40
+ const result = await origin(requestOrigin, c);
41
+ if (result === true || result === void 0) originValue = requestOrigin;
42
+ else if (result === false) originValue = null;
43
+ else if (typeof result === "string") originValue = result;
44
+ } else originValue = configureOrigin(c, origin, requestOrigin);
45
+ if (originValue) {
46
+ c.header("Access-Control-Allow-Origin", originValue);
47
+ if (originValue !== "*" || credentials) c.header("Vary", "Origin");
48
+ }
49
+ if (credentials) c.header("Access-Control-Allow-Credentials", "true");
50
+ if (exposeHeaders.length > 0) c.header("Access-Control-Expose-Headers", exposeHeaders.join(", "));
51
+ if (isPreflight(c)) {
52
+ const preflightHeaders = {};
53
+ if (originValue) preflightHeaders["Access-Control-Allow-Origin"] = originValue;
54
+ if (credentials) preflightHeaders["Access-Control-Allow-Credentials"] = "true";
55
+ preflightHeaders["Access-Control-Allow-Methods"] = allowMethods.join(", ");
56
+ preflightHeaders["Access-Control-Allow-Headers"] = resolvedAllowHeaders.join(", ");
57
+ if (maxAge != null) preflightHeaders["Access-Control-Max-Age"] = String(maxAge);
58
+ if (originValue && originValue !== "*") preflightHeaders["Vary"] = "Origin";
59
+ if (!preflightContinue) return new Response(null, {
60
+ status: 204,
61
+ headers: preflightHeaders
62
+ });
63
+ }
64
+ await next();
65
+ };
66
+ }
67
+ function defineCors(options) {
68
+ return createCorsMiddleware(options);
69
+ }
70
+ //#endregion
71
+ //#region src/rate-limit.ts
72
+ var MemoryRateLimitStore = class {
73
+ store = /* @__PURE__ */ new Map();
74
+ timer = null;
75
+ constructor() {
76
+ this.timer = setInterval(() => this._cleanup(), 6e4);
77
+ }
78
+ _cleanup() {
79
+ const now = Date.now();
80
+ for (const [key, entry] of this.store.entries()) if (entry.expireAt <= now) this.store.delete(key);
81
+ }
82
+ async get(key) {
83
+ const entry = this.store.get(key);
84
+ if (!entry || entry.expireAt <= Date.now()) {
85
+ if (entry) this.store.delete(key);
86
+ return;
87
+ }
88
+ return {
89
+ count: entry.count,
90
+ resetAt: entry.resetAt
91
+ };
92
+ }
93
+ async set(key, entry, ttlMs) {
94
+ this.store.set(key, {
95
+ ...entry,
96
+ expireAt: Date.now() + ttlMs
97
+ });
98
+ }
99
+ async increment(key, windowMs) {
100
+ const now = Date.now();
101
+ const existing = this.store.get(key);
102
+ if (!existing || existing.resetAt <= now) {
103
+ const entry = {
104
+ count: 1,
105
+ resetAt: now + windowMs,
106
+ expireAt: now + windowMs
107
+ };
108
+ this.store.set(key, entry);
109
+ return {
110
+ count: 1,
111
+ resetAt: entry.resetAt
112
+ };
113
+ }
114
+ existing.count++;
115
+ return {
116
+ count: existing.count,
117
+ resetAt: existing.resetAt
118
+ };
119
+ }
120
+ async reset(key) {
121
+ this.store.delete(key);
122
+ }
123
+ destroy() {
124
+ if (this.timer) {
125
+ clearInterval(this.timer);
126
+ this.timer = null;
127
+ }
128
+ this.store.clear();
129
+ }
130
+ };
131
+ function defaultKeyGenerator(c) {
132
+ const forwarded = c.req.header("x-forwarded-for");
133
+ if (forwarded) return forwarded.split(",")[0].trim();
134
+ const realIp = c.req.header("x-real-ip");
135
+ if (realIp) return realIp;
136
+ return c.req.raw.headers.get("cf-connecting-ip") || "unknown";
137
+ }
138
+ function defaultHandler(c, info) {
139
+ c.header("Retry-After", String(Math.ceil(info.retryAfter / 1e3)));
140
+ return c.json({
141
+ error: "Too Many Requests",
142
+ message: `Rate limit exceeded. Try again in ${Math.ceil(info.retryAfter / 1e3)} seconds.`,
143
+ limit: info.limit,
144
+ remaining: info.remaining,
145
+ reset: info.reset
146
+ }, 429);
147
+ }
148
+ function createRateLimitMiddleware(options = {}) {
149
+ const { maxRequests = 100, windowMs = 6e4, keyGenerator = defaultKeyGenerator, handler = defaultHandler, skip, standardHeaders = true, legacyHeaders = false, store: customStore } = options;
150
+ const store = customStore || new MemoryRateLimitStore();
151
+ return async function rateLimitMiddleware(c, next) {
152
+ if (skip && await skip(c)) {
153
+ await next();
154
+ return;
155
+ }
156
+ const key = keyGenerator(c);
157
+ const result = await store.increment(key, windowMs);
158
+ const now = Date.now();
159
+ const remaining = Math.max(0, maxRequests - result.count);
160
+ const reset = Math.ceil(result.resetAt / 1e3);
161
+ const retryAfter = Math.max(0, result.resetAt - now);
162
+ if (standardHeaders) {
163
+ c.header("RateLimit-Limit", String(maxRequests));
164
+ c.header("RateLimit-Remaining", String(remaining));
165
+ c.header("RateLimit-Reset", String(reset));
166
+ }
167
+ if (legacyHeaders) {
168
+ c.header("X-RateLimit-Limit", String(maxRequests));
169
+ c.header("X-RateLimit-Remaining", String(remaining));
170
+ c.header("X-RateLimit-Reset", String(reset));
171
+ }
172
+ if (result.count > maxRequests) return handler(c, {
173
+ limit: maxRequests,
174
+ remaining: 0,
175
+ reset,
176
+ retryAfter
177
+ });
178
+ await next();
179
+ };
180
+ }
181
+ function defineRateLimit(options) {
182
+ return createRateLimitMiddleware(options);
183
+ }
184
+ function createMemoryRateLimitStore() {
185
+ return new MemoryRateLimitStore();
186
+ }
187
+ //#endregion
188
+ //#region src/after.ts
189
+ /**
190
+ * 使用 AsyncLocalStorage 实现请求作用域
191
+ * 在非 Node.js 环境中降级为基于 requestId 的 Map
192
+ */
193
+ let asyncLocalStorage$1 = null;
194
+ try {
195
+ const { AsyncLocalStorage } = await import("node:async_hooks");
196
+ asyncLocalStorage$1 = new AsyncLocalStorage();
197
+ } catch {}
198
+ /**
199
+ * Fallback: 使用全局 Map(基于请求 ID,不如 AsyncLocalStorage 精确)
200
+ */
201
+ const fallbackStore$1 = /* @__PURE__ */ new Map();
202
+ function getRequestId$1(c) {
203
+ return c.get("requestId") || "default";
204
+ }
205
+ /**
206
+ * 注册一个在响应发送后执行的回调
207
+ *
208
+ * @example
209
+ * ```typescript
210
+ * import { after } from 'ubean';
211
+ *
212
+ * export const POST = defineHandler(async (c) => {
213
+ * const result = doWork();
214
+ * after(() => {
215
+ * // 这些不会阻塞响应
216
+ * analytics.track('post_created', result);
217
+ * invalidateCache('/posts');
218
+ * });
219
+ * return c.json(result);
220
+ * });
221
+ * ```
222
+ */
223
+ function after(callback) {
224
+ if (asyncLocalStorage$1) {
225
+ const ctx = asyncLocalStorage$1.getStore?.();
226
+ if (ctx && ctx.registered) {
227
+ ctx.callbacks.push(callback);
228
+ return;
229
+ }
230
+ }
231
+ if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") console.warn("[ubean] after() called outside of after middleware context, callback will not be executed");
232
+ }
233
+ /**
234
+ * 创建 after() 中间件
235
+ *
236
+ * 在响应发送后执行所有通过 `after()` 注册的回调
237
+ */
238
+ function createAfterMiddleware() {
239
+ return async function afterMiddleware(c, next) {
240
+ const ctx = {
241
+ callbacks: [],
242
+ registered: true
243
+ };
244
+ const reqId = getRequestId$1(c);
245
+ fallbackStore$1.set(reqId, ctx);
246
+ if (asyncLocalStorage$1) await asyncLocalStorage$1.run(ctx, async () => {
247
+ await next();
248
+ });
249
+ else await next();
250
+ const callbacks = ctx.callbacks;
251
+ fallbackStore$1.delete(reqId);
252
+ if (callbacks.length > 0) {
253
+ const executeCallbacks = async () => {
254
+ for (const cb of callbacks) try {
255
+ await cb();
256
+ } catch (err) {
257
+ console.error("[ubean] after() callback error:", err);
258
+ }
259
+ };
260
+ const waitUntil = c.executionContext?.waitUntil;
261
+ if (typeof waitUntil === "function") waitUntil(executeCallbacks());
262
+ else if (typeof globalThis.queueMicrotask === "function") globalThis.queueMicrotask(() => {
263
+ executeCallbacks().catch((err) => console.error("[ubean] after() error:", err));
264
+ });
265
+ else setTimeout(() => {
266
+ executeCallbacks().catch((err) => console.error("[ubean] after() error:", err));
267
+ }, 0);
268
+ }
269
+ };
270
+ }
271
+ /**
272
+ * 手动执行 after 回调(用于自定义中间件或测试)
273
+ */
274
+ async function flushAfterCallbacks(c) {
275
+ const reqId = getRequestId$1(c);
276
+ const ctx = fallbackStore$1.get(reqId);
277
+ if (!ctx) return;
278
+ for (const cb of ctx.callbacks) try {
279
+ await cb();
280
+ } catch (err) {
281
+ console.error("[ubean] after() callback error:", err);
282
+ }
283
+ ctx.callbacks = [];
284
+ fallbackStore$1.delete(reqId);
285
+ }
286
+ /**
287
+ * 获取当前请求中注册的 after 回调数量(主要用于测试)
288
+ */
289
+ function getAfterCallbackCount(c) {
290
+ if (asyncLocalStorage$1) {
291
+ const store = asyncLocalStorage$1.getStore?.();
292
+ if (store) return store.callbacks.length;
293
+ }
294
+ if (c) {
295
+ const ctx = fallbackStore$1.get(getRequestId$1(c));
296
+ if (ctx) return ctx.callbacks.length;
297
+ }
298
+ return 0;
299
+ }
300
+ //#endregion
301
+ //#region src/draft-mode.ts
302
+ function sign(value, secret) {
303
+ return `${value}.${createHmac("sha256", secret).update(value).digest("base64url")}`;
304
+ }
305
+ function verify(signed, secret) {
306
+ const idx = signed.lastIndexOf(".");
307
+ if (idx === -1) return null;
308
+ const value = signed.slice(0, idx);
309
+ const signature = signed.slice(idx + 1);
310
+ const expected = createHmac("sha256", secret).update(value).digest("base64url");
311
+ if (signature.length !== expected.length) return null;
312
+ try {
313
+ if (timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) return value;
314
+ } catch {
315
+ return null;
316
+ }
317
+ return null;
318
+ }
319
+ function parseCookie(cookieHeader, name) {
320
+ if (!cookieHeader) return void 0;
321
+ const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`));
322
+ return match ? match[1] : void 0;
323
+ }
324
+ function serializeCookie(name, value, opts, maxAge) {
325
+ const parts = [`${name}=${value}`];
326
+ if (opts.path) parts.push(`Path=${opts.path}`);
327
+ if (opts.domain) parts.push(`Domain=${opts.domain}`);
328
+ if (opts.secure) parts.push("Secure");
329
+ if (opts.httpOnly) parts.push("HttpOnly");
330
+ if (opts.sameSite) parts.push(`SameSite=${opts.sameSite}`);
331
+ if (maxAge !== void 0) parts.push(`Max-Age=${maxAge}`);
332
+ return parts.join("; ");
333
+ }
334
+ function isExcluded(path, exclude) {
335
+ if (!exclude || exclude.length === 0) return false;
336
+ return exclude.some((p) => path.startsWith(p));
337
+ }
338
+ const DRAFT_MODE_CONTEXT_KEY = "__ubean_draft_mode__";
339
+ var DraftModeController = class {
340
+ _enabled;
341
+ _pendingAction = "none";
342
+ constructor(initialEnabled) {
343
+ this._enabled = initialEnabled;
344
+ }
345
+ get isEnabled() {
346
+ return this._enabled;
347
+ }
348
+ get pendingAction() {
349
+ return this._pendingAction;
350
+ }
351
+ enable() {
352
+ this._enabled = true;
353
+ this._pendingAction = "enable";
354
+ }
355
+ disable() {
356
+ this._enabled = false;
357
+ this._pendingAction = "disable";
358
+ }
359
+ };
360
+ function getController(c) {
361
+ return c[DRAFT_MODE_CONTEXT_KEY] || null;
362
+ }
363
+ /**
364
+ * 创建 draft mode 中间件
365
+ *
366
+ * 中间件读取请求中的签名 cookie,验证签名与过期时间后在 context 上
367
+ * 标记 draft mode 状态。响应阶段根据 controller 的 pendingAction
368
+ * 设置或清除 cookie。
369
+ *
370
+ * @example
371
+ * ```typescript
372
+ * // 启用 draft mode(默认 1 小时有效)
373
+ * app.use('*', createDraftModeMiddleware({ secret: 'my-secret' }));
374
+ *
375
+ * // 自定义 cookie 名称与 TTL
376
+ * app.use('*', createDraftModeMiddleware({
377
+ * secret: 'my-secret',
378
+ * cookieName: 'preview',
379
+ * ttl: 86400
380
+ * }));
381
+ * ```
382
+ */
383
+ function createDraftModeMiddleware(options) {
384
+ if (!options.secret) throw new Error("[ubean] createDraftModeMiddleware requires a `secret` option");
385
+ const { cookieName = "ubean_draft", ttl = 3600, cookie: cookieOpts = {}, exclude = [] } = options;
386
+ const cookieDefaults = {
387
+ path: "/",
388
+ secure: false,
389
+ httpOnly: true,
390
+ sameSite: "lax",
391
+ ...cookieOpts
392
+ };
393
+ return async function draftModeMiddleware(c, next) {
394
+ if (isExcluded(c.req.path, exclude)) {
395
+ await next();
396
+ return;
397
+ }
398
+ const cookieValue = parseCookie(c.req.header("cookie"), cookieName);
399
+ let enabled = false;
400
+ if (cookieValue) {
401
+ const verified = verify(cookieValue, options.secret);
402
+ if (verified !== null) {
403
+ const expiry = Number(verified);
404
+ if (!Number.isNaN(expiry) && expiry > Date.now()) enabled = true;
405
+ }
406
+ }
407
+ const controller = new DraftModeController(enabled);
408
+ c[DRAFT_MODE_CONTEXT_KEY] = controller;
409
+ await next();
410
+ if (controller.pendingAction === "enable") {
411
+ const expiry = Date.now() + ttl * 1e3;
412
+ const token = sign(String(expiry), options.secret);
413
+ c.header("Set-Cookie", serializeCookie(cookieName, token, cookieDefaults, ttl));
414
+ } else if (controller.pendingAction === "disable") c.header("Set-Cookie", serializeCookie(cookieName, "", cookieDefaults, 0));
415
+ };
416
+ }
417
+ /**
418
+ * 启用 draft mode(设置签名 cookie)
419
+ *
420
+ * 在路由处理函数中调用,中间件会在响应阶段设置 cookie。
421
+ *
422
+ * @example
423
+ * ```typescript
424
+ * app.get('/api/preview/enable', c => {
425
+ * enableDraftMode(c);
426
+ * return c.json({ ok: true });
427
+ * });
428
+ * ```
429
+ */
430
+ function enableDraftMode(c) {
431
+ const controller = getController(c);
432
+ if (!controller) throw new Error("[ubean] enableDraftMode() requires createDraftModeMiddleware to be registered on this app");
433
+ controller.enable();
434
+ }
435
+ /**
436
+ * 禁用 draft mode(清除 cookie)
437
+ *
438
+ * @example
439
+ * ```typescript
440
+ * app.get('/api/preview/disable', c => {
441
+ * disableDraftMode(c);
442
+ * return c.json({ ok: true });
443
+ * });
444
+ * ```
445
+ */
446
+ function disableDraftMode(c) {
447
+ const controller = getController(c);
448
+ if (!controller) throw new Error("[ubean] disableDraftMode() requires createDraftModeMiddleware to be registered on this app");
449
+ controller.disable();
450
+ }
451
+ /**
452
+ * 检查 draft mode 是否已启用(从 context 读取)
453
+ *
454
+ * 未注册中间件时返回 false。
455
+ */
456
+ function isDraftMode(c) {
457
+ return getController(c)?.isEnabled ?? false;
458
+ }
459
+ /**
460
+ * 获取 draft mode 组合式 API
461
+ *
462
+ * 返回 `{ isEnabled, enable, disable }`,在路由处理函数中使用。
463
+ *
464
+ * @example
465
+ * ```typescript
466
+ * app.get('*', c => {
467
+ * const draft = useDraftMode(c);
468
+ * if (draft.isEnabled) {
469
+ * // 返回草稿内容
470
+ * }
471
+ * return c.json({ draft: draft.isEnabled });
472
+ * });
473
+ * ```
474
+ */
475
+ function useDraftMode(c) {
476
+ const controller = getController(c);
477
+ if (controller) return controller;
478
+ return {
479
+ isEnabled: false,
480
+ enable: () => {
481
+ throw new Error("[ubean] useDraftMode().enable() requires createDraftModeMiddleware to be registered on this app");
482
+ },
483
+ disable: () => {
484
+ throw new Error("[ubean] useDraftMode().disable() requires createDraftModeMiddleware to be registered on this app");
485
+ }
486
+ };
487
+ }
488
+ /**
489
+ * 定义 draft mode 中间件(别名,与 defineCsrf / defineCors 风格一致)
490
+ */
491
+ function defineDraftMode(options) {
492
+ return createDraftModeMiddleware(options);
493
+ }
494
+ //#endregion
495
+ //#region src/single-flight.ts
496
+ let asyncLocalStorage = null;
497
+ try {
498
+ const { AsyncLocalStorage } = await import("node:async_hooks");
499
+ asyncLocalStorage = new AsyncLocalStorage();
500
+ } catch {}
501
+ /**
502
+ * Fallback: 使用全局 Map(基于 requestId,不如 AsyncLocalStorage 精确)
503
+ */
504
+ const fallbackStore = /* @__PURE__ */ new Map();
505
+ function getRequestId(c) {
506
+ return c.get("requestId") || "default";
507
+ }
508
+ function getCurrentContext(c) {
509
+ if (asyncLocalStorage) return asyncLocalStorage.getStore?.();
510
+ if (c) return fallbackStore.get(getRequestId(c));
511
+ }
512
+ /**
513
+ * 全局 revalidation 处理器注册表(进程内内存)。
514
+ *
515
+ * 每个 entry 声明它负责哪些 revalidation 键。当 `invalidate(keys)` 被调用时,
516
+ * 中间件查找所有键与 invalidated 键有交集的 entry 并执行其 fetcher。
517
+ */
518
+ const revalidationRegistry = [];
519
+ /**
520
+ * 注册一个 revalidation 依赖。
521
+ *
522
+ * @param keys 该 fetcher 负责的重新获取键列表
523
+ * @param fetcher 重新获取处理器,返回需要打包进响应的数据
524
+ * @param name 可选名称(用于调试)
525
+ *
526
+ * @example
527
+ * ```typescript
528
+ * defineRevalidation(['users', 'user-count'], async (ctx) => {
529
+ * const [users, count] = await Promise.all([
530
+ * db.query.users.findMany(),
531
+ * db.select({ count: count() }).from(users)
532
+ * ]);
533
+ * return { users, 'user-count': count };
534
+ * });
535
+ * ```
536
+ */
537
+ function defineRevalidation(keys, fetcher, name) {
538
+ const entry = {
539
+ keys: [...keys],
540
+ fetcher,
541
+ name
542
+ };
543
+ revalidationRegistry.push(entry);
544
+ return entry;
545
+ }
546
+ /**
547
+ * 取消注册一个 revalidation 条目。
548
+ */
549
+ function unregisterRevalidation(entry) {
550
+ const idx = revalidationRegistry.indexOf(entry);
551
+ if (idx === -1) return false;
552
+ revalidationRegistry.splice(idx, 1);
553
+ return true;
554
+ }
555
+ /**
556
+ * 获取所有已注册的 revalidation 条目(只读副本,用于测试/调试)。
557
+ */
558
+ function getRevalidationEntries() {
559
+ return [...revalidationRegistry];
560
+ }
561
+ /**
562
+ * 清空所有 revalidation 注册(主要用于测试)。
563
+ */
564
+ function clearRevalidationRegistry() {
565
+ revalidationRegistry.length = 0;
566
+ }
567
+ /**
568
+ * 标记需要重新获取的依赖键。
569
+ *
570
+ * 必须在 single-flight 中间件作用域内(action 处理器中)调用,
571
+ * 否则给出开发警告。
572
+ *
573
+ * @example
574
+ * ```typescript
575
+ * export const deleteUser = defineAction(async (input) => {
576
+ * await db.delete(users).where(eq(users.id, input.id));
577
+ * invalidate(['users', `user:${input.id}`]);
578
+ * return { ok: true };
579
+ * });
580
+ * ```
581
+ */
582
+ function invalidate(keys) {
583
+ const ctx = asyncLocalStorage?.getStore?.();
584
+ if (ctx && ctx.registered) {
585
+ for (const key of keys) ctx.invalidatedKeys.add(key);
586
+ return;
587
+ }
588
+ if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") console.warn("[ubean] invalidate() called outside of single-flight middleware context, revalidation will not be executed");
589
+ }
590
+ /**
591
+ * 标记单个键需要重新获取(`invalidate([key])` 的简写)。
592
+ */
593
+ function invalidateKey(key) {
594
+ invalidate([key]);
595
+ }
596
+ /**
597
+ * 获取当前请求中被 invalidate 的键(主要用于测试/调试)。
598
+ */
599
+ function getInvalidatedKeys(c) {
600
+ const ctx = getCurrentContext(c);
601
+ if (!ctx) return [];
602
+ return Array.from(ctx.invalidatedKeys);
603
+ }
604
+ /**
605
+ * 查找所有键与 invalidated 键有交集的 entry。
606
+ */
607
+ function findMatchingEntries(invalidatedKeys) {
608
+ if (invalidatedKeys.length === 0) return [];
609
+ const keySet = new Set(invalidatedKeys);
610
+ return revalidationRegistry.filter((entry) => entry.keys.some((k) => keySet.has(k)));
611
+ }
612
+ /**
613
+ * 执行所有匹配的 revalidation fetcher 并收集结果。
614
+ */
615
+ async function executeRevalidation(invalidatedKeys, c, parallel) {
616
+ const entries = findMatchingEntries(invalidatedKeys);
617
+ const data = {};
618
+ const errors = [];
619
+ const touchedKeys = /* @__PURE__ */ new Set();
620
+ const runFetcher = async (entry) => {
621
+ const matchedKeys = entry.keys.filter((k) => invalidatedKeys.includes(k));
622
+ for (const k of matchedKeys) touchedKeys.add(k);
623
+ try {
624
+ const result = await entry.fetcher({
625
+ keys: matchedKeys,
626
+ context: c
627
+ });
628
+ if (result && typeof result === "object") Object.assign(data, result);
629
+ } catch (err) {
630
+ errors.push({
631
+ keys: entry.keys,
632
+ message: err instanceof Error ? err.message : String(err)
633
+ });
634
+ }
635
+ };
636
+ if (parallel) await Promise.all(entries.map(runFetcher));
637
+ else for (const entry of entries) await runFetcher(entry);
638
+ return {
639
+ data,
640
+ keys: Array.from(touchedKeys),
641
+ timestamp: Date.now(),
642
+ errors
643
+ };
644
+ }
645
+ /**
646
+ * 创建 single-flight mutations 中间件。
647
+ *
648
+ * 拦截 Server Action 响应,执行由 `invalidate()` 标记的 revalidation,
649
+ * 将结果打包进响应体,使客户端在单个 round-trip 内获得更新后的数据。
650
+ *
651
+ * @example
652
+ * ```typescript
653
+ * app.use('*', createSingleFlightMiddleware());
654
+ * ```
655
+ */
656
+ function createSingleFlightMiddleware(options = {}) {
657
+ const { revalidationHeader = "X-Ubean-Revalidated", revalidationField = "__ubean_revalidation__", contentTypes = ["application/json"], skip, parallel = true } = options;
658
+ return async function singleFlightMiddleware(c, next) {
659
+ if (skip && await skip(c)) {
660
+ await next();
661
+ return;
662
+ }
663
+ const ctx = {
664
+ invalidatedKeys: /* @__PURE__ */ new Set(),
665
+ registered: true
666
+ };
667
+ const reqId = getRequestId(c);
668
+ fallbackStore.set(reqId, ctx);
669
+ if (asyncLocalStorage) await asyncLocalStorage.run(ctx, async () => {
670
+ await next();
671
+ });
672
+ else await next();
673
+ fallbackStore.delete(reqId);
674
+ const invalidatedKeys = Array.from(ctx.invalidatedKeys);
675
+ if (invalidatedKeys.length === 0) return;
676
+ const result = await executeRevalidation(invalidatedKeys, c, parallel);
677
+ const response = c.res;
678
+ const contentType = response.headers.get("content-type") || "";
679
+ if (!contentTypes.some((ct) => contentType.includes(ct))) {
680
+ c.header(revalidationHeader, invalidatedKeys.join(","));
681
+ return;
682
+ }
683
+ try {
684
+ const originalBody = await response.json();
685
+ const newBody = {
686
+ ...originalBody && typeof originalBody === "object" ? originalBody : { value: originalBody },
687
+ [revalidationField]: result
688
+ };
689
+ const newResponse = c.json(newBody, response.status);
690
+ response.headers.forEach((value, key) => {
691
+ if (key.toLowerCase() !== "content-type" && key.toLowerCase() !== "content-length") newResponse.headers.set(key, value);
692
+ });
693
+ newResponse.headers.set(revalidationHeader, result.keys.join(","));
694
+ c.res = newResponse;
695
+ } catch {
696
+ c.header(revalidationHeader, invalidatedKeys.join(","));
697
+ }
698
+ };
699
+ }
700
+ /**
701
+ * 手动执行 revalidation(用于自定义中间件或测试)。
702
+ *
703
+ * 在中间件作用域外也可调用 —— 直接传入 invalidated 键与上下文。
704
+ */
705
+ async function runRevalidation(invalidatedKeys, c, parallel = true) {
706
+ return executeRevalidation(invalidatedKeys, c, parallel);
707
+ }
708
+ /**
709
+ * Single-flight 中间件别名(与 cors/rate-limit 的 define* 风格一致)。
710
+ */
711
+ function defineSingleFlight(options = {}) {
712
+ return createSingleFlightMiddleware(options);
713
+ }
714
+ //#endregion
715
+ export { defineRateLimit as C, createRateLimitMiddleware as S, defineCors as T, after as _, getInvalidatedKeys as a, getAfterCallbackCount as b, invalidateKey as c, createDraftModeMiddleware as d, defineDraftMode as f, useDraftMode as g, isDraftMode as h, defineSingleFlight as i, runRevalidation as l, enableDraftMode as m, createSingleFlightMiddleware as n, getRevalidationEntries as o, disableDraftMode as p, defineRevalidation as r, invalidate as s, clearRevalidationRegistry as t, unregisterRevalidation as u, createAfterMiddleware as v, createCorsMiddleware as w, createMemoryRateLimitStore as x, flushAfterCallbacks as y };