alepha 0.20.6 → 0.20.7

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 (243) hide show
  1. package/AGENTS.md +0 -1
  2. package/CLAUDE.md +0 -1
  3. package/assets/agents-template.md +0 -1
  4. package/dist/api/audits/index.browser.js +1 -0
  5. package/dist/api/audits/index.browser.js.map +1 -1
  6. package/dist/api/audits/index.d.ts +370 -355
  7. package/dist/api/audits/index.d.ts.map +1 -1
  8. package/dist/api/audits/index.js +1 -0
  9. package/dist/api/audits/index.js.map +1 -1
  10. package/dist/api/files/index.browser.js +1 -0
  11. package/dist/api/files/index.browser.js.map +1 -1
  12. package/dist/api/files/index.d.ts +179 -170
  13. package/dist/api/files/index.d.ts.map +1 -1
  14. package/dist/api/files/index.js +1 -0
  15. package/dist/api/files/index.js.map +1 -1
  16. package/dist/api/jobs/index.browser.js +7 -0
  17. package/dist/api/jobs/index.browser.js.map +1 -1
  18. package/dist/api/jobs/index.d.ts +271 -262
  19. package/dist/api/jobs/index.d.ts.map +1 -1
  20. package/dist/api/jobs/index.js +21 -3
  21. package/dist/api/jobs/index.js.map +1 -1
  22. package/dist/api/keys/index.d.ts +198 -192
  23. package/dist/api/keys/index.d.ts.map +1 -1
  24. package/dist/api/keys/index.js +1 -0
  25. package/dist/api/keys/index.js.map +1 -1
  26. package/dist/api/notifications/index.d.ts +246 -245
  27. package/dist/api/notifications/index.d.ts.map +1 -1
  28. package/dist/api/organizations/index.d.ts +100 -97
  29. package/dist/api/organizations/index.d.ts.map +1 -1
  30. package/dist/api/parameters/index.d.ts +323 -320
  31. package/dist/api/parameters/index.d.ts.map +1 -1
  32. package/dist/api/payments/index.d.ts +431 -376
  33. package/dist/api/payments/index.d.ts.map +1 -1
  34. package/dist/api/payments/index.js +202 -87
  35. package/dist/api/payments/index.js.map +1 -1
  36. package/dist/api/subscriptions/index.d.ts +1695 -0
  37. package/dist/api/subscriptions/index.d.ts.map +1 -0
  38. package/dist/api/subscriptions/index.js +1919 -0
  39. package/dist/api/subscriptions/index.js.map +1 -0
  40. package/dist/api/users/index.d.ts +863 -847
  41. package/dist/api/users/index.d.ts.map +1 -1
  42. package/dist/api/verifications/index.d.ts +126 -125
  43. package/dist/api/verifications/index.d.ts.map +1 -1
  44. package/dist/bucket/index.d.ts +3 -2
  45. package/dist/bucket/index.d.ts.map +1 -1
  46. package/dist/cache/core/index.d.ts +114 -4
  47. package/dist/cache/core/index.d.ts.map +1 -1
  48. package/dist/cache/core/index.js +181 -15
  49. package/dist/cache/core/index.js.map +1 -1
  50. package/dist/cache/core/index.workerd.js +181 -15
  51. package/dist/cache/core/index.workerd.js.map +1 -1
  52. package/dist/cache/database/index.d.ts +20 -19
  53. package/dist/cache/database/index.d.ts.map +1 -1
  54. package/dist/cache/redis/index.d.ts +3 -2
  55. package/dist/cache/redis/index.d.ts.map +1 -1
  56. package/dist/cli/core/index.d.ts +113 -129
  57. package/dist/cli/core/index.d.ts.map +1 -1
  58. package/dist/cli/core/index.js +75 -7
  59. package/dist/cli/core/index.js.map +1 -1
  60. package/dist/cli/devtools/index.d.ts +3 -2
  61. package/dist/cli/devtools/index.d.ts.map +1 -1
  62. package/dist/cli/platform/index.d.ts +346 -290
  63. package/dist/cli/platform/index.d.ts.map +1 -1
  64. package/dist/cli/platform/index.js +105 -6
  65. package/dist/cli/platform/index.js.map +1 -1
  66. package/dist/cli/vendor/index.d.ts +12 -11
  67. package/dist/cli/vendor/index.d.ts.map +1 -1
  68. package/dist/command/index.d.ts +5 -4
  69. package/dist/command/index.d.ts.map +1 -1
  70. package/dist/core/index.browser.js +1 -1
  71. package/dist/core/index.browser.js.map +1 -1
  72. package/dist/core/index.d.ts +119 -118
  73. package/dist/core/index.d.ts.map +1 -1
  74. package/dist/core/index.js +1 -1
  75. package/dist/core/index.js.map +1 -1
  76. package/dist/core/index.native.js +1 -1
  77. package/dist/core/index.native.js.map +1 -1
  78. package/dist/core/index.workerd.js +1 -1
  79. package/dist/core/index.workerd.js.map +1 -1
  80. package/dist/crypto/index.d.ts +3 -2
  81. package/dist/crypto/index.d.ts.map +1 -1
  82. package/dist/email/core/index.d.ts +3 -2
  83. package/dist/email/core/index.d.ts.map +1 -1
  84. package/dist/email/smtp/index.d.ts +7 -6
  85. package/dist/email/smtp/index.d.ts.map +1 -1
  86. package/dist/lock/core/index.d.ts +5 -4
  87. package/dist/lock/core/index.d.ts.map +1 -1
  88. package/dist/logger/index.d.ts +10 -9
  89. package/dist/logger/index.d.ts.map +1 -1
  90. package/dist/mcp/index.d.ts +9 -8
  91. package/dist/mcp/index.d.ts.map +1 -1
  92. package/dist/mcp/index.js +1 -1
  93. package/dist/mcp/index.js.map +1 -1
  94. package/dist/orm/core/index.browser.js +9 -3
  95. package/dist/orm/core/index.browser.js.map +1 -1
  96. package/dist/orm/core/index.bun.js +31 -10
  97. package/dist/orm/core/index.bun.js.map +1 -1
  98. package/dist/orm/core/index.d.ts +33 -14
  99. package/dist/orm/core/index.d.ts.map +1 -1
  100. package/dist/orm/core/index.js +31 -10
  101. package/dist/orm/core/index.js.map +1 -1
  102. package/dist/orm/postgres/index.d.ts +6 -5
  103. package/dist/orm/postgres/index.d.ts.map +1 -1
  104. package/dist/queue/core/index.d.ts +5 -4
  105. package/dist/queue/core/index.d.ts.map +1 -1
  106. package/dist/queue/redis/index.d.ts +3 -2
  107. package/dist/queue/redis/index.d.ts.map +1 -1
  108. package/dist/react/form/index.d.ts +5 -0
  109. package/dist/react/form/index.d.ts.map +1 -1
  110. package/dist/react/form/index.js +6 -4
  111. package/dist/react/form/index.js.map +1 -1
  112. package/dist/react/i18n/index.d.ts +2 -1
  113. package/dist/react/i18n/index.d.ts.map +1 -1
  114. package/dist/react/router/index.d.ts +206 -205
  115. package/dist/react/router/index.d.ts.map +1 -1
  116. package/dist/react/ui/index.d.ts +11 -11
  117. package/dist/react/ui/index.d.ts.map +1 -1
  118. package/dist/scheduler/index.d.ts +3 -2
  119. package/dist/scheduler/index.d.ts.map +1 -1
  120. package/dist/security/index.browser.js +29 -1
  121. package/dist/security/index.browser.js.map +1 -1
  122. package/dist/security/index.d.ts +82 -35
  123. package/dist/security/index.d.ts.map +1 -1
  124. package/dist/security/index.js +56 -3
  125. package/dist/security/index.js.map +1 -1
  126. package/dist/server/auth/index.d.ts +163 -158
  127. package/dist/server/auth/index.d.ts.map +1 -1
  128. package/dist/server/auth/index.js +16 -4
  129. package/dist/server/auth/index.js.map +1 -1
  130. package/dist/server/core/index.d.ts +35 -34
  131. package/dist/server/core/index.d.ts.map +1 -1
  132. package/dist/server/cors/index.d.ts +7 -6
  133. package/dist/server/cors/index.d.ts.map +1 -1
  134. package/dist/server/health/index.d.ts +16 -15
  135. package/dist/server/health/index.d.ts.map +1 -1
  136. package/dist/server/links/index.d.ts +51 -50
  137. package/dist/server/links/index.d.ts.map +1 -1
  138. package/dist/server/rate-limit/index.d.ts +6 -5
  139. package/dist/server/rate-limit/index.d.ts.map +1 -1
  140. package/dist/server/swagger/index.d.ts +2 -1
  141. package/dist/server/swagger/index.d.ts.map +1 -1
  142. package/dist/topic/redis/index.d.ts +3 -2
  143. package/dist/topic/redis/index.d.ts.map +1 -1
  144. package/package.json +16 -32
  145. package/src/api/audits/entities/audits.ts +1 -0
  146. package/src/api/files/entities/files.ts +1 -0
  147. package/src/api/jobs/__tests__/$job.spec.ts +92 -40
  148. package/src/api/jobs/entities/jobExecutionEntity.ts +1 -0
  149. package/src/api/jobs/providers/JobProvider.ts +20 -5
  150. package/src/api/jobs/schemas/jobConfigAtom.ts +5 -0
  151. package/src/api/keys/entities/apiKeyEntity.ts +1 -0
  152. package/src/api/payments/controllers/MockCheckoutController.ts +146 -0
  153. package/src/api/payments/index.ts +3 -0
  154. package/src/api/payments/providers/MemoryPaymentProvider.ts +9 -4
  155. package/src/api/payments/providers/PaymentProvider.ts +25 -9
  156. package/src/api/payments/services/PaymentService.ts +3 -0
  157. package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
  158. package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
  159. package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
  160. package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
  161. package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
  162. package/src/api/subscriptions/entities/subscriptions.ts +68 -0
  163. package/src/api/subscriptions/index.ts +133 -0
  164. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
  165. package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
  166. package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
  167. package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
  168. package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
  169. package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
  170. package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
  171. package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
  172. package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
  173. package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
  174. package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
  175. package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
  176. package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
  177. package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
  178. package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
  179. package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
  180. package/src/api/subscriptions/services/BillingService.ts +437 -0
  181. package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
  182. package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
  183. package/src/api/subscriptions/services/UsageService.ts +118 -0
  184. package/src/cache/core/__tests__/$cache.memory.spec.ts +450 -0
  185. package/src/cache/core/__tests__/$cache.swr.spec.ts +394 -0
  186. package/src/cache/core/index.ts +16 -0
  187. package/src/cache/core/primitives/$cache.ts +347 -21
  188. package/src/cli/core/tasks/BuildCloudflareTask.ts +16 -0
  189. package/src/cli/core/templates/agentMd.ts +39 -4
  190. package/src/cli/core/templates/biomeJson.ts +25 -1
  191. package/src/cli/core/templates/saasAdminLayoutTsx.ts +2 -2
  192. package/src/cli/platform/__tests__/CloudflareAdapter.spec.ts +117 -0
  193. package/src/cli/platform/adapters/CloudflareAdapter.ts +104 -7
  194. package/src/cli/platform/atoms/platformOptions.ts +13 -0
  195. package/src/cli/platform/schemas/platform.ts +1 -0
  196. package/src/cli/platform/services/CloudflareApi.ts +61 -0
  197. package/src/cli/platform/services/PlatformOrchestrator.ts +9 -4
  198. package/src/core/__tests__/$module.spec.ts +2 -2
  199. package/src/core/primitives/$module.ts +4 -4
  200. package/src/mcp/providers/McpServerProvider.ts +1 -1
  201. package/src/orm/core/providers/DatabaseTypeProvider.ts +9 -3
  202. package/src/orm/core/providers/drivers/DatabaseProvider.ts +1 -1
  203. package/src/orm/core/schemas/insertSchema.ts +10 -2
  204. package/src/orm/core/services/Repository.ts +27 -7
  205. package/src/react/form/hooks/useFormState.ts +8 -1
  206. package/src/react/form/index.ts +10 -1
  207. package/src/react/form/services/FormModel.ts +9 -3
  208. package/src/security/atoms/currentTenantAtom.ts +34 -0
  209. package/src/security/index.browser.ts +1 -0
  210. package/src/security/index.ts +12 -1
  211. package/src/security/primitives/$issuer.ts +17 -1
  212. package/src/security/providers/SecurityProvider.ts +37 -0
  213. package/src/server/auth/__tests__/validateRedirectUri.spec.ts +78 -0
  214. package/src/server/auth/providers/ServerAuthProvider.ts +21 -5
  215. package/tsconfig.base.json +2 -1
  216. package/dist/react/websocket/index.d.ts +0 -117
  217. package/dist/react/websocket/index.d.ts.map +0 -1
  218. package/dist/react/websocket/index.js +0 -108
  219. package/dist/react/websocket/index.js.map +0 -1
  220. package/dist/websocket/index.browser.js +0 -848
  221. package/dist/websocket/index.browser.js.map +0 -1
  222. package/dist/websocket/index.d.ts +0 -876
  223. package/dist/websocket/index.d.ts.map +0 -1
  224. package/dist/websocket/index.js +0 -1185
  225. package/dist/websocket/index.js.map +0 -1
  226. package/src/react/websocket/hooks/useRoom.tsx +0 -251
  227. package/src/react/websocket/index.ts +0 -7
  228. package/src/websocket/__tests__/$channel.spec.ts +0 -30
  229. package/src/websocket/__tests__/$websocket-new.spec.ts +0 -195
  230. package/src/websocket/__tests__/RoomManager.spec.ts +0 -146
  231. package/src/websocket/__tests__/websocket-integration.spec.ts +0 -951
  232. package/src/websocket/errors/WebSocketError.ts +0 -34
  233. package/src/websocket/index.browser.ts +0 -25
  234. package/src/websocket/index.shared.ts +0 -8
  235. package/src/websocket/index.ts +0 -85
  236. package/src/websocket/interfaces/WebSocketInterfaces.ts +0 -252
  237. package/src/websocket/primitives/$channel.ts +0 -131
  238. package/src/websocket/primitives/$websocket.ts +0 -107
  239. package/src/websocket/providers/NodeWebSocketServerProvider.ts +0 -617
  240. package/src/websocket/providers/WebSocketServerProvider.ts +0 -56
  241. package/src/websocket/services/RoomManager.ts +0 -160
  242. package/src/websocket/services/WebSocketClient.ts +0 -642
  243. package/src/websocket/services/WebSocketTopicService.ts +0 -108
@@ -66,10 +66,18 @@ export function $cache(options: any = {}): any {
66
66
  const mw: any = <T extends (...args: any[]) => any>(handler: T): T => {
67
67
  return (async (...args: any[]) => {
68
68
  const key = instance.key(...args);
69
- const cached = await instance.get(key);
70
- if (cached !== undefined) return cached;
71
-
72
- const result = await handler(...args);
69
+ const read = await instance.read(key);
70
+
71
+ if (read.value !== undefined) {
72
+ if (read.stale) {
73
+ instance.scheduleRefresh(key, () => handler(...args));
74
+ }
75
+ return read.value;
76
+ }
77
+
78
+ const result = await instance.runSingleFlight(key, () =>
79
+ handler(...args),
80
+ );
73
81
  // Fire-and-forget — cache write failures must not break the handler result
74
82
  instance.set(key, result).catch(() => {});
75
83
  return result;
@@ -86,6 +94,34 @@ export function $cache(options: any = {}): any {
86
94
 
87
95
  // ---------------------------------------------------------------------------------------------------------------------
88
96
 
97
+ /**
98
+ * Options for the in-memory L1 tier.
99
+ */
100
+ export interface CacheMemoryTierOptions {
101
+ /**
102
+ * TTL for the in-memory tier. Should be ≤ the remote `ttl`.
103
+ * Bounds the cross-isolate staleness window after invalidation.
104
+ *
105
+ * @default min(ttl, 30s)
106
+ */
107
+ ttl?: DurationLike;
108
+
109
+ /**
110
+ * LRU bound — max entries kept in memory before eviction.
111
+ *
112
+ * @default 500
113
+ */
114
+ max?: number;
115
+
116
+ /**
117
+ * Also cache provider misses (`undefined`) in memory for this duration.
118
+ * Prevents hammering the remote tier on cold/unknown keys.
119
+ *
120
+ * @default off
121
+ */
122
+ negative?: DurationLike;
123
+ }
124
+
89
125
  export interface CachePrimitiveOptions<
90
126
  TReturn = any,
91
127
  TParameter extends any[] = any[],
@@ -151,6 +187,35 @@ export interface CachePrimitiveOptions<
151
187
  * Reduces storage size by 60-80% for JSON payloads at the cost of CPU.
152
188
  */
153
189
  compress?: boolean;
190
+
191
+ /**
192
+ * Add an in-process L1 memory tier in front of `provider`.
193
+ *
194
+ * Reads check memory first, fall back to the provider on miss. Writes go
195
+ * to both tiers (write-through), so own-writes are immediately visible.
196
+ *
197
+ * Caveats:
198
+ * - Per-process only. Each Worker isolate / Node process has its own L1.
199
+ * `invalidate()` clears the local L1 + the remote provider; other
200
+ * processes keep their L1 until its TTL expires.
201
+ * - Use a short L1 TTL to bound the cross-isolate staleness window.
202
+ *
203
+ * @default off
204
+ */
205
+ memory?: true | CacheMemoryTierOptions;
206
+
207
+ /**
208
+ * Stale-while-revalidate window. After `ttl` expires, the cached value
209
+ * remains servable for `stale` longer; reads in this window return the
210
+ * stale value immediately and trigger ONE background refresh
211
+ * (single-flight per key).
212
+ *
213
+ * Requires a `handler` (primitive mode) OR middleware mode wrapping a
214
+ * handler — the cache needs to know how to recompute.
215
+ *
216
+ * @default off
217
+ */
218
+ stale?: DurationLike;
154
219
  }
155
220
 
156
221
  // ---------------------------------------------------------------------------------------------------------------------
@@ -186,6 +251,27 @@ declare module "alepha" {
186
251
 
187
252
  // ---------------------------------------------------------------------------------------------------------------------
188
253
 
254
+ const DEFAULT_MEMORY_MAX = 500;
255
+ const DEFAULT_MEMORY_TTL_MS = 30_000;
256
+ const SWR_MARKER = "__swr" as const;
257
+
258
+ type SwrEnvelope = {
259
+ [SWR_MARKER]: 1;
260
+ v: unknown;
261
+ f: number;
262
+ };
263
+
264
+ type L1Entry<T> = {
265
+ value: T | undefined;
266
+ expiresAt: number;
267
+ negative: boolean;
268
+ };
269
+
270
+ type ReadResult<T> = {
271
+ value: T | undefined;
272
+ stale: boolean;
273
+ };
274
+
189
275
  export class CachePrimitive<
190
276
  TReturn = any,
191
277
  TParameter extends any[] = any[],
@@ -194,6 +280,46 @@ export class CachePrimitive<
194
280
  protected readonly dateTimeProvider = $inject(DateTimeProvider);
195
281
  public readonly provider = this.$provider();
196
282
 
283
+ protected readonly memoryStore?: Map<string, L1Entry<TReturn>>;
284
+ protected readonly memoryMax: number = DEFAULT_MEMORY_MAX;
285
+ protected readonly memoryTtlMs: number = 0;
286
+ protected readonly negativeTtlMs: number = 0;
287
+
288
+ protected readonly inflightRefreshes = new Map<string, Promise<TReturn>>();
289
+
290
+ constructor(
291
+ args: ConstructorParameters<
292
+ typeof Primitive<CachePrimitiveOptions<TReturn, TParameter>>
293
+ >[0],
294
+ ) {
295
+ super(args);
296
+ const mem = this.options.memory;
297
+ if (mem) {
298
+ this.memoryStore = new Map();
299
+ const memOpts: CacheMemoryTierOptions = mem === true ? {} : mem;
300
+ this.memoryMax = memOpts.max ?? DEFAULT_MEMORY_MAX;
301
+ // Default L1 TTL: min(remote ttl, 30s). If remote ttl is 0/infinite, use 30s.
302
+ if (memOpts.ttl !== undefined) {
303
+ this.memoryTtlMs = this.dateTimeProvider
304
+ .duration(memOpts.ttl)
305
+ .as("milliseconds");
306
+ } else {
307
+ const remoteTtlMs = this.options.ttl
308
+ ? this.dateTimeProvider.duration(this.options.ttl).as("milliseconds")
309
+ : 0;
310
+ this.memoryTtlMs =
311
+ remoteTtlMs > 0
312
+ ? Math.min(remoteTtlMs, DEFAULT_MEMORY_TTL_MS)
313
+ : DEFAULT_MEMORY_TTL_MS;
314
+ }
315
+ if (memOpts.negative !== undefined) {
316
+ this.negativeTtlMs = this.dateTimeProvider
317
+ .duration(memOpts.negative)
318
+ .as("milliseconds");
319
+ }
320
+ }
321
+ }
322
+
197
323
  public get container(): string {
198
324
  return (
199
325
  this.options.name ??
@@ -208,16 +334,18 @@ export class CachePrimitive<
208
334
  }
209
335
 
210
336
  const key = this.key(...args);
211
- const cached = await this.get(key);
212
- if (cached !== undefined) {
213
- return cached;
337
+ const read = await this.read(key);
338
+
339
+ if (read.value !== undefined) {
340
+ if (read.stale) {
341
+ this.scheduleRefresh(key, () => handler(...args));
342
+ }
343
+ return read.value;
214
344
  }
215
345
 
216
- const result = await handler(...args);
346
+ const result = await this.runSingleFlight(key, () => handler(...args));
217
347
  // note: when exception occurs, don't cache the result
218
-
219
348
  await this.set(key, result);
220
-
221
349
  return result;
222
350
  }
223
351
 
@@ -226,10 +354,29 @@ export class CachePrimitive<
226
354
  }
227
355
 
228
356
  public async incr(key: string, amount = 1): Promise<number> {
229
- return this.provider.incr(this.container, key, amount);
357
+ const result = await this.provider.incr(this.container, key, amount);
358
+ // L1 is no longer authoritative after atomic incr on remote.
359
+ this.delL1(key);
360
+ return result;
230
361
  }
231
362
 
232
363
  public async invalidate(...keys: string[]): Promise<void> {
364
+ if (this.memoryStore) {
365
+ if (keys.length === 0) {
366
+ this.memoryStore.clear();
367
+ } else {
368
+ for (const key of keys) {
369
+ if (key.endsWith("*")) {
370
+ const prefix = key.slice(0, -1);
371
+ for (const k of this.memoryStore.keys()) {
372
+ if (k.startsWith(prefix)) this.memoryStore.delete(k);
373
+ }
374
+ } else {
375
+ this.memoryStore.delete(key);
376
+ }
377
+ }
378
+ }
379
+ }
233
380
  await this.provider.invalidateKeys(this.container, keys);
234
381
  }
235
382
 
@@ -246,41 +393,193 @@ export class CachePrimitive<
246
393
  return;
247
394
  }
248
395
 
249
- const px = this.dateTimeProvider
396
+ const freshTtlMs = this.dateTimeProvider
250
397
  .duration(
251
398
  ttl ?? this.options.ttl ?? [this.settings.defaultTtl, "seconds"],
252
399
  )
253
400
  .as("milliseconds");
254
401
 
255
- await this.provider.setTyped(this.container, key, value, {
256
- ttl: px > 0 ? px : undefined,
402
+ const staleMs = this.options.stale
403
+ ? this.dateTimeProvider.duration(this.options.stale).as("milliseconds")
404
+ : 0;
405
+
406
+ const providerTtlMs = freshTtlMs > 0 ? freshTtlMs + staleMs : 0;
407
+ const now = this.dateTimeProvider.nowMillis();
408
+ const freshUntil = freshTtlMs > 0 ? now + freshTtlMs : 0;
409
+
410
+ const payload =
411
+ this.options.stale && freshTtlMs > 0
412
+ ? ({ [SWR_MARKER]: 1, v: value, f: freshUntil } satisfies SwrEnvelope)
413
+ : value;
414
+
415
+ await this.provider.setTyped(this.container, key, payload, {
416
+ ttl: providerTtlMs > 0 ? providerTtlMs : undefined,
257
417
  compress: this.options.compress,
258
418
  });
259
419
 
420
+ // Write-through to L1 (raw value, not wrapped).
421
+ this.setL1(key, {
422
+ value,
423
+ expiresAt:
424
+ this.memoryTtlMs > 0
425
+ ? now + this.memoryTtlMs
426
+ : Number.POSITIVE_INFINITY,
427
+ negative: false,
428
+ });
429
+
430
+ // A fresh write supersedes any pending refresh for this key.
431
+ this.inflightRefreshes.delete(key);
432
+
260
433
  await this.alepha.events.emit("cache:set", {
261
434
  container: this.container,
262
435
  key,
263
- ttlMs: px > 0 ? px : undefined,
436
+ ttlMs: providerTtlMs > 0 ? providerTtlMs : undefined,
264
437
  });
265
438
  }
266
439
 
267
440
  public async get(key: string): Promise<TReturn | undefined> {
441
+ const read = await this.read(key);
442
+ return read.value;
443
+ }
444
+
445
+ /**
446
+ * Internal read that also reports whether the value is stale (SWR
447
+ * grace window). Middleware and `run()` use this to decide whether to
448
+ * schedule a background refresh.
449
+ */
450
+ public async read(key: string): Promise<ReadResult<TReturn>> {
268
451
  if (
269
452
  !this.alepha.isStarted() ||
270
453
  this.options.disabled ||
271
454
  !this.settings.enabled
272
455
  ) {
273
- return undefined;
456
+ return { value: undefined, stale: false };
274
457
  }
275
458
 
276
- const value = await this.provider.getTyped<TReturn>(this.container, key);
459
+ const now = this.dateTimeProvider.nowMillis();
460
+
461
+ // L1 check
462
+ if (this.memoryStore) {
463
+ const entry = this.memoryStore.get(key);
464
+ if (entry !== undefined) {
465
+ if (entry.expiresAt > now) {
466
+ // LRU touch
467
+ this.memoryStore.delete(key);
468
+ this.memoryStore.set(key, entry);
469
+ await this.alepha.events.emit("cache:hit", {
470
+ container: this.container,
471
+ key,
472
+ });
473
+ return {
474
+ value: entry.negative ? undefined : entry.value,
475
+ stale: false,
476
+ };
477
+ }
478
+ this.memoryStore.delete(key);
479
+ }
480
+ }
277
481
 
278
- await this.alepha.events.emit(
279
- value === undefined ? "cache:miss" : "cache:hit",
280
- { container: this.container, key },
482
+ // L2 check
483
+ const raw = await this.provider.getTyped<TReturn | SwrEnvelope>(
484
+ this.container,
485
+ key,
281
486
  );
282
487
 
283
- return value;
488
+ if (raw === undefined) {
489
+ // Negative caching
490
+ if (this.memoryStore && this.negativeTtlMs > 0) {
491
+ this.setL1(key, {
492
+ value: undefined,
493
+ expiresAt: now + this.negativeTtlMs,
494
+ negative: true,
495
+ });
496
+ }
497
+ await this.alepha.events.emit("cache:miss", {
498
+ container: this.container,
499
+ key,
500
+ });
501
+ return { value: undefined, stale: false };
502
+ }
503
+
504
+ let value: TReturn;
505
+ let stale = false;
506
+
507
+ if (this.isSwrEnvelope(raw)) {
508
+ value = raw.v as TReturn;
509
+ stale = raw.f > 0 && raw.f <= now;
510
+ } else {
511
+ value = raw as TReturn;
512
+ }
513
+
514
+ // Populate L1 (write-back from L2 read)
515
+ if (this.memoryStore && this.memoryTtlMs > 0) {
516
+ this.setL1(key, {
517
+ value,
518
+ expiresAt: now + this.memoryTtlMs,
519
+ negative: false,
520
+ });
521
+ }
522
+
523
+ await this.alepha.events.emit(stale ? "cache:stale" : "cache:hit", {
524
+ container: this.container,
525
+ key,
526
+ });
527
+
528
+ return { value, stale };
529
+ }
530
+
531
+ /**
532
+ * Run a handler under single-flight: concurrent callers for the same
533
+ * key share one in-flight promise.
534
+ */
535
+ public async runSingleFlight(
536
+ key: string,
537
+ handler: () => Promise<TReturn> | TReturn,
538
+ ): Promise<TReturn> {
539
+ const existing = this.inflightRefreshes.get(key);
540
+ if (existing) {
541
+ return existing;
542
+ }
543
+ const promise = (async () => {
544
+ try {
545
+ return await handler();
546
+ } finally {
547
+ this.inflightRefreshes.delete(key);
548
+ }
549
+ })();
550
+ this.inflightRefreshes.set(key, promise);
551
+ return promise;
552
+ }
553
+
554
+ /**
555
+ * Schedule a background refresh for a stale key. At most one refresh
556
+ * per key is in-flight at any time; failures are swallowed (the stale
557
+ * value keeps being served until expiry).
558
+ */
559
+ public scheduleRefresh(
560
+ key: string,
561
+ handler: () => Promise<TReturn> | TReturn,
562
+ ): void {
563
+ if (this.inflightRefreshes.has(key)) {
564
+ return;
565
+ }
566
+ const promise = (async () => {
567
+ try {
568
+ const result = await handler();
569
+ await this.set(key, result);
570
+ await this.alepha.events.emit("cache:revalidate", {
571
+ container: this.container,
572
+ key,
573
+ });
574
+ return result;
575
+ } finally {
576
+ this.inflightRefreshes.delete(key);
577
+ }
578
+ })();
579
+ promise.catch(() => {
580
+ // swallow: stale value keeps serving until expiry
581
+ });
582
+ this.inflightRefreshes.set(key, promise);
284
583
  }
285
584
 
286
585
  protected $provider(): CacheProvider {
@@ -294,6 +593,33 @@ export class CachePrimitive<
294
593
 
295
594
  return this.alepha.inject(this.options.provider);
296
595
  }
596
+
597
+ protected isSwrEnvelope(value: unknown): value is SwrEnvelope {
598
+ return (
599
+ value !== null &&
600
+ typeof value === "object" &&
601
+ (value as Record<string, unknown>)[SWR_MARKER] === 1 &&
602
+ "f" in (value as Record<string, unknown>) &&
603
+ "v" in (value as Record<string, unknown>)
604
+ );
605
+ }
606
+
607
+ protected setL1(key: string, entry: L1Entry<TReturn>): void {
608
+ if (!this.memoryStore) return;
609
+ if (this.memoryStore.has(key)) {
610
+ this.memoryStore.delete(key);
611
+ }
612
+ this.memoryStore.set(key, entry);
613
+ while (this.memoryStore.size > this.memoryMax) {
614
+ const first = this.memoryStore.keys().next().value;
615
+ if (first === undefined) break;
616
+ this.memoryStore.delete(first);
617
+ }
618
+ }
619
+
620
+ protected delL1(key: string): void {
621
+ this.memoryStore?.delete(key);
622
+ }
297
623
  }
298
624
 
299
625
  export interface CachePrimitiveFn<
@@ -99,6 +99,22 @@ export class BuildCloudflareTask extends BuildTask {
99
99
  return;
100
100
  }
101
101
 
102
+ if (domain.includes("*")) {
103
+ const zone = process.env.CLOUDFLARE_ZONE;
104
+ if (!zone) {
105
+ throw new Error(
106
+ `Wildcard domain "${domain}" requires CLOUDFLARE_ZONE to be set (the parent zone name, e.g. "alepha.dev").`,
107
+ );
108
+ }
109
+ wrangler.routes = [
110
+ {
111
+ pattern: domain.endsWith("/*") ? domain : `${domain}/*`,
112
+ zone_name: zone,
113
+ },
114
+ ];
115
+ return;
116
+ }
117
+
102
118
  wrangler.routes = [
103
119
  {
104
120
  pattern: domain,
@@ -13,12 +13,47 @@ This is an **Alepha** project.
13
13
  ## Commands
14
14
 
15
15
  \`\`\`bash
16
- alepha lint # Format and lint
17
- alepha typecheck # Type checking
18
- alepha test # Run tests
19
- alepha build # Build
16
+ alepha lint # Format and lint
17
+ alepha typecheck # Type checking
18
+ alepha test # Run tests
19
+ alepha build # Build
20
+ alepha platform plan # Show planned cloud topology (requires platform plugin)
21
+ alepha platform up # Provision + deploy to a configured environment
22
+ alepha platform status # Inspect deployed resources
20
23
  \`\`\`
21
24
 
25
+ ## Cloud deployment (Cloudflare Workers)
26
+
27
+ Add the \`platform\` plugin to \`alepha.config.ts\` to manage cloud
28
+ provisioning, deploy, secrets, and DB migrations end-to-end:
29
+
30
+ \`\`\`ts
31
+ import { defineConfig } from "alepha/cli/config";
32
+ import { platform } from "alepha/cli/platform";
33
+
34
+ export default defineConfig({
35
+ plugins: [
36
+ platform({
37
+ environments: {
38
+ production: {
39
+ adapter: "cloudflare",
40
+ domain: "yourapp.com",
41
+ // zone: "yourapp.com", // required only for wildcard domains
42
+ // jurisdiction: "eu", // optional: EU data residency
43
+ },
44
+ },
45
+ }),
46
+ ],
47
+ });
48
+ \`\`\`
49
+
50
+ Then: \`alepha platform up --env production\` (auth via \`wrangler login\` on first run).
51
+
52
+ Supported adapters: \`cloudflare\`, \`vercel\`. The Cloudflare adapter provisions
53
+ D1 (or Hyperdrive when \`DATABASE_URL\` is postgres), KV, R2, Queues, and pushes
54
+ secrets via \`wrangler secret bulk\`. Set \`build.target: "cloudflare"\` in
55
+ \`alepha.config.ts\` if you only want the build artifact without the orchestrator.
56
+
22
57
  ## Documentation
23
58
 
24
59
  - Framework source: \`node_modules/alepha/src/\`
@@ -17,12 +17,36 @@ export const biomeJson = () =>
17
17
  "linter": {
18
18
  "enabled": true,
19
19
  "rules": {
20
- "recommended": true
20
+ "recommended": true,
21
+ "a11y": {
22
+ "useFocusableInteractive": "off",
23
+ "useSemanticElements": "off",
24
+ "useKeyWithClickEvents": "off",
25
+ "useAriaPropsForRole": "off",
26
+ "noLabelWithoutControl": "off"
27
+ },
28
+ "correctness": {
29
+ "useExhaustiveDependencies": "off"
30
+ },
31
+ "suspicious": {
32
+ "noArrayIndexKey": "off",
33
+ "noExplicitAny": "off",
34
+ "noDocumentCookie": "off"
35
+ },
36
+ "style": {
37
+ "noNonNullAssertion": "off"
38
+ }
21
39
  },
22
40
  "domains": {
23
41
  "react": "recommended"
24
42
  }
25
43
  },
44
+ "css": {
45
+ "parser": {
46
+ "cssModules": false,
47
+ "tailwindDirectives": true
48
+ }
49
+ },
26
50
  "assist": {
27
51
  "actions": {
28
52
  "source": {
@@ -8,10 +8,10 @@
8
8
  * `@/components/*` alias.
9
9
  */
10
10
  export const saasAdminLayoutTsx = () =>
11
- `import { AppShell } from "@/components/app-shell";
11
+ `import { AppShell } from "@/components/app-shell/app-shell";
12
12
  import { Toaster } from "@/components/ui/sonner";
13
13
  import { TooltipProvider } from "@/components/ui/tooltip";
14
- import { DialogProvider } from "@/components/use-dialog";
14
+ import { DialogProvider } from "@/components/use-dialog/use-dialog";
15
15
  import { NestedView, useRouterState } from "alepha/react/router";
16
16
  import { ShieldCheck, Users } from "lucide-react";
17
17