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.
- package/AGENTS.md +0 -1
- package/CLAUDE.md +0 -1
- package/assets/agents-template.md +0 -1
- package/dist/api/audits/index.browser.js +1 -0
- package/dist/api/audits/index.browser.js.map +1 -1
- package/dist/api/audits/index.d.ts +370 -355
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +1 -0
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.browser.js +1 -0
- package/dist/api/files/index.browser.js.map +1 -1
- package/dist/api/files/index.d.ts +179 -170
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +1 -0
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.browser.js +7 -0
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +271 -262
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +21 -3
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.d.ts +198 -192
- package/dist/api/keys/index.d.ts.map +1 -1
- package/dist/api/keys/index.js +1 -0
- package/dist/api/keys/index.js.map +1 -1
- package/dist/api/notifications/index.d.ts +246 -245
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/organizations/index.d.ts +100 -97
- package/dist/api/organizations/index.d.ts.map +1 -1
- package/dist/api/parameters/index.d.ts +323 -320
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/payments/index.d.ts +431 -376
- package/dist/api/payments/index.d.ts.map +1 -1
- package/dist/api/payments/index.js +202 -87
- package/dist/api/payments/index.js.map +1 -1
- package/dist/api/subscriptions/index.d.ts +1695 -0
- package/dist/api/subscriptions/index.d.ts.map +1 -0
- package/dist/api/subscriptions/index.js +1919 -0
- package/dist/api/subscriptions/index.js.map +1 -0
- package/dist/api/users/index.d.ts +863 -847
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/verifications/index.d.ts +126 -125
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/bucket/index.d.ts +3 -2
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/cache/core/index.d.ts +114 -4
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/core/index.js +181 -15
- package/dist/cache/core/index.js.map +1 -1
- package/dist/cache/core/index.workerd.js +181 -15
- package/dist/cache/core/index.workerd.js.map +1 -1
- package/dist/cache/database/index.d.ts +20 -19
- package/dist/cache/database/index.d.ts.map +1 -1
- package/dist/cache/redis/index.d.ts +3 -2
- package/dist/cache/redis/index.d.ts.map +1 -1
- package/dist/cli/core/index.d.ts +113 -129
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +75 -7
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/devtools/index.d.ts +3 -2
- package/dist/cli/devtools/index.d.ts.map +1 -1
- package/dist/cli/platform/index.d.ts +346 -290
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +105 -6
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/vendor/index.d.ts +12 -11
- package/dist/cli/vendor/index.d.ts.map +1 -1
- package/dist/command/index.d.ts +5 -4
- package/dist/command/index.d.ts.map +1 -1
- package/dist/core/index.browser.js +1 -1
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +119 -118
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +1 -1
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +1 -1
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/crypto/index.d.ts +3 -2
- package/dist/crypto/index.d.ts.map +1 -1
- package/dist/email/core/index.d.ts +3 -2
- package/dist/email/core/index.d.ts.map +1 -1
- package/dist/email/smtp/index.d.ts +7 -6
- package/dist/email/smtp/index.d.ts.map +1 -1
- package/dist/lock/core/index.d.ts +5 -4
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/logger/index.d.ts +10 -9
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/mcp/index.d.ts +9 -8
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js +9 -3
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js +31 -10
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +33 -14
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +31 -10
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.d.ts +6 -5
- package/dist/orm/postgres/index.d.ts.map +1 -1
- package/dist/queue/core/index.d.ts +5 -4
- package/dist/queue/core/index.d.ts.map +1 -1
- package/dist/queue/redis/index.d.ts +3 -2
- package/dist/queue/redis/index.d.ts.map +1 -1
- package/dist/react/form/index.d.ts +5 -0
- package/dist/react/form/index.d.ts.map +1 -1
- package/dist/react/form/index.js +6 -4
- package/dist/react/form/index.js.map +1 -1
- package/dist/react/i18n/index.d.ts +2 -1
- package/dist/react/i18n/index.d.ts.map +1 -1
- package/dist/react/router/index.d.ts +206 -205
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/ui/index.d.ts +11 -11
- package/dist/react/ui/index.d.ts.map +1 -1
- package/dist/scheduler/index.d.ts +3 -2
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/security/index.browser.js +29 -1
- package/dist/security/index.browser.js.map +1 -1
- package/dist/security/index.d.ts +82 -35
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +56 -3
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +163 -158
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +16 -4
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/core/index.d.ts +35 -34
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/cors/index.d.ts +7 -6
- package/dist/server/cors/index.d.ts.map +1 -1
- package/dist/server/health/index.d.ts +16 -15
- package/dist/server/health/index.d.ts.map +1 -1
- package/dist/server/links/index.d.ts +51 -50
- package/dist/server/links/index.d.ts.map +1 -1
- package/dist/server/rate-limit/index.d.ts +6 -5
- package/dist/server/rate-limit/index.d.ts.map +1 -1
- package/dist/server/swagger/index.d.ts +2 -1
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/topic/redis/index.d.ts +3 -2
- package/dist/topic/redis/index.d.ts.map +1 -1
- package/package.json +16 -32
- package/src/api/audits/entities/audits.ts +1 -0
- package/src/api/files/entities/files.ts +1 -0
- package/src/api/jobs/__tests__/$job.spec.ts +92 -40
- package/src/api/jobs/entities/jobExecutionEntity.ts +1 -0
- package/src/api/jobs/providers/JobProvider.ts +20 -5
- package/src/api/jobs/schemas/jobConfigAtom.ts +5 -0
- package/src/api/keys/entities/apiKeyEntity.ts +1 -0
- package/src/api/payments/controllers/MockCheckoutController.ts +146 -0
- package/src/api/payments/index.ts +3 -0
- package/src/api/payments/providers/MemoryPaymentProvider.ts +9 -4
- package/src/api/payments/providers/PaymentProvider.ts +25 -9
- package/src/api/payments/services/PaymentService.ts +3 -0
- package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
- package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
- package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
- package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
- package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
- package/src/api/subscriptions/entities/subscriptions.ts +68 -0
- package/src/api/subscriptions/index.ts +133 -0
- package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
- package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
- package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
- package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
- package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
- package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
- package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
- package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
- package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
- package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
- package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
- package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
- package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
- package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
- package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
- package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
- package/src/api/subscriptions/services/BillingService.ts +437 -0
- package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
- package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
- package/src/api/subscriptions/services/UsageService.ts +118 -0
- package/src/cache/core/__tests__/$cache.memory.spec.ts +450 -0
- package/src/cache/core/__tests__/$cache.swr.spec.ts +394 -0
- package/src/cache/core/index.ts +16 -0
- package/src/cache/core/primitives/$cache.ts +347 -21
- package/src/cli/core/tasks/BuildCloudflareTask.ts +16 -0
- package/src/cli/core/templates/agentMd.ts +39 -4
- package/src/cli/core/templates/biomeJson.ts +25 -1
- package/src/cli/core/templates/saasAdminLayoutTsx.ts +2 -2
- package/src/cli/platform/__tests__/CloudflareAdapter.spec.ts +117 -0
- package/src/cli/platform/adapters/CloudflareAdapter.ts +104 -7
- package/src/cli/platform/atoms/platformOptions.ts +13 -0
- package/src/cli/platform/schemas/platform.ts +1 -0
- package/src/cli/platform/services/CloudflareApi.ts +61 -0
- package/src/cli/platform/services/PlatformOrchestrator.ts +9 -4
- package/src/core/__tests__/$module.spec.ts +2 -2
- package/src/core/primitives/$module.ts +4 -4
- package/src/mcp/providers/McpServerProvider.ts +1 -1
- package/src/orm/core/providers/DatabaseTypeProvider.ts +9 -3
- package/src/orm/core/providers/drivers/DatabaseProvider.ts +1 -1
- package/src/orm/core/schemas/insertSchema.ts +10 -2
- package/src/orm/core/services/Repository.ts +27 -7
- package/src/react/form/hooks/useFormState.ts +8 -1
- package/src/react/form/index.ts +10 -1
- package/src/react/form/services/FormModel.ts +9 -3
- package/src/security/atoms/currentTenantAtom.ts +34 -0
- package/src/security/index.browser.ts +1 -0
- package/src/security/index.ts +12 -1
- package/src/security/primitives/$issuer.ts +17 -1
- package/src/security/providers/SecurityProvider.ts +37 -0
- package/src/server/auth/__tests__/validateRedirectUri.spec.ts +78 -0
- package/src/server/auth/providers/ServerAuthProvider.ts +21 -5
- package/tsconfig.base.json +2 -1
- package/dist/react/websocket/index.d.ts +0 -117
- package/dist/react/websocket/index.d.ts.map +0 -1
- package/dist/react/websocket/index.js +0 -108
- package/dist/react/websocket/index.js.map +0 -1
- package/dist/websocket/index.browser.js +0 -848
- package/dist/websocket/index.browser.js.map +0 -1
- package/dist/websocket/index.d.ts +0 -876
- package/dist/websocket/index.d.ts.map +0 -1
- package/dist/websocket/index.js +0 -1185
- package/dist/websocket/index.js.map +0 -1
- package/src/react/websocket/hooks/useRoom.tsx +0 -251
- package/src/react/websocket/index.ts +0 -7
- package/src/websocket/__tests__/$channel.spec.ts +0 -30
- package/src/websocket/__tests__/$websocket-new.spec.ts +0 -195
- package/src/websocket/__tests__/RoomManager.spec.ts +0 -146
- package/src/websocket/__tests__/websocket-integration.spec.ts +0 -951
- package/src/websocket/errors/WebSocketError.ts +0 -34
- package/src/websocket/index.browser.ts +0 -25
- package/src/websocket/index.shared.ts +0 -8
- package/src/websocket/index.ts +0 -85
- package/src/websocket/interfaces/WebSocketInterfaces.ts +0 -252
- package/src/websocket/primitives/$channel.ts +0 -131
- package/src/websocket/primitives/$websocket.ts +0 -107
- package/src/websocket/providers/NodeWebSocketServerProvider.ts +0 -617
- package/src/websocket/providers/WebSocketServerProvider.ts +0 -56
- package/src/websocket/services/RoomManager.ts +0 -160
- package/src/websocket/services/WebSocketClient.ts +0 -642
- 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
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
256
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
482
|
+
// L2 check
|
|
483
|
+
const raw = await this.provider.getTyped<TReturn | SwrEnvelope>(
|
|
484
|
+
this.container,
|
|
485
|
+
key,
|
|
281
486
|
);
|
|
282
487
|
|
|
283
|
-
|
|
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
|
|
17
|
-
alepha typecheck
|
|
18
|
-
alepha test
|
|
19
|
-
alepha 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
|
|