alepha 0.20.6 → 0.20.8
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 +259 -250
- 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 +857 -841
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/verifications/index.d.ts +128 -127
- 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 +116 -132
- 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
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { $inject } from "alepha";
|
|
2
|
+
import { CacheProvider } from "alepha/cache";
|
|
3
|
+
import { DateTimeProvider } from "alepha/datetime";
|
|
4
|
+
import { SubscriptionService } from "./SubscriptionService.ts";
|
|
5
|
+
|
|
6
|
+
// -----------------------------------------------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The result of a usage check or increment operation.
|
|
10
|
+
*/
|
|
11
|
+
export interface UsageResult {
|
|
12
|
+
/**
|
|
13
|
+
* Whether the operation is allowed given the current usage and limit.
|
|
14
|
+
*/
|
|
15
|
+
allowed: boolean;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Current usage count for the period.
|
|
19
|
+
*/
|
|
20
|
+
current: number;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The plan limit for the resource. -1 means unlimited.
|
|
24
|
+
*/
|
|
25
|
+
limit: number;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Remaining capacity. -1 means unlimited.
|
|
29
|
+
*/
|
|
30
|
+
remaining: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// -----------------------------------------------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Tracks and enforces per-organization resource usage limits.
|
|
37
|
+
*
|
|
38
|
+
* Usage counters are keyed by `organizationId:resource:YYYY-MM` and stored in the cache.
|
|
39
|
+
* Limits are resolved from the organization's current subscription plan.
|
|
40
|
+
*/
|
|
41
|
+
export class UsageService {
|
|
42
|
+
protected readonly cache = $inject(CacheProvider);
|
|
43
|
+
protected readonly dateTime = $inject(DateTimeProvider);
|
|
44
|
+
protected readonly subscriptionService = $inject(SubscriptionService);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Increment a resource counter for the current period and return the usage result.
|
|
48
|
+
*
|
|
49
|
+
* @param organizationId The organization to track usage for.
|
|
50
|
+
* @param resource The resource identifier (e.g., "api_calls", "seats").
|
|
51
|
+
* @param amount Amount to increment by (default: 1).
|
|
52
|
+
*/
|
|
53
|
+
public async increment(
|
|
54
|
+
organizationId: string,
|
|
55
|
+
resource: string,
|
|
56
|
+
amount = 1,
|
|
57
|
+
): Promise<UsageResult> {
|
|
58
|
+
const limit = await this.subscriptionService.limit(
|
|
59
|
+
organizationId,
|
|
60
|
+
resource,
|
|
61
|
+
);
|
|
62
|
+
const key = this.buildKey(organizationId, resource);
|
|
63
|
+
const current = await this.cache.incr("subscriptions:usage", key, amount);
|
|
64
|
+
const allowed = limit === -1 || current <= limit;
|
|
65
|
+
return {
|
|
66
|
+
allowed,
|
|
67
|
+
current,
|
|
68
|
+
limit,
|
|
69
|
+
remaining: limit === -1 ? -1 : Math.max(0, limit - current),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get the current usage for a resource without incrementing.
|
|
75
|
+
*
|
|
76
|
+
* @param organizationId The organization to query usage for.
|
|
77
|
+
* @param resource The resource identifier.
|
|
78
|
+
*/
|
|
79
|
+
public async getUsage(
|
|
80
|
+
organizationId: string,
|
|
81
|
+
resource: string,
|
|
82
|
+
): Promise<UsageResult> {
|
|
83
|
+
const limit = await this.subscriptionService.limit(
|
|
84
|
+
organizationId,
|
|
85
|
+
resource,
|
|
86
|
+
);
|
|
87
|
+
const key = this.buildKey(organizationId, resource);
|
|
88
|
+
const current = await this.cache.incr("subscriptions:usage", key, 0);
|
|
89
|
+
return {
|
|
90
|
+
allowed: limit === -1 || current <= limit,
|
|
91
|
+
current,
|
|
92
|
+
limit,
|
|
93
|
+
remaining: limit === -1 ? -1 : Math.max(0, limit - current),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Reset all usage counters for an organization.
|
|
99
|
+
*
|
|
100
|
+
* Used at the start of a new billing period.
|
|
101
|
+
*
|
|
102
|
+
* @param organizationId The organization whose counters to reset.
|
|
103
|
+
*/
|
|
104
|
+
public async resetForPeriod(organizationId: string): Promise<void> {
|
|
105
|
+
const pattern = this.buildKey(organizationId, "*");
|
|
106
|
+
await this.cache.invalidateKeys("subscriptions:usage", [pattern]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build the cache key for a usage counter.
|
|
111
|
+
*
|
|
112
|
+
* Format: `organizationId:resource:YYYY-MM`
|
|
113
|
+
*/
|
|
114
|
+
protected buildKey(organizationId: string, resource: string): string {
|
|
115
|
+
const period = this.dateTime.now().format("YYYY-MM");
|
|
116
|
+
return `${organizationId}:${resource}:${period}`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { Alepha } from "alepha";
|
|
2
|
+
import { DateTimeProvider } from "alepha/datetime";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { $cache, CacheProvider, MemoryCacheProvider } from "../index.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tests for the in-process L1 memory tier (`memory` option).
|
|
8
|
+
*
|
|
9
|
+
* The L1 tier sits in front of the configured `CacheProvider`. Reads check
|
|
10
|
+
* memory first; writes are write-through to both tiers. The L1 store is
|
|
11
|
+
* per-primitive and uses lazy expiry + LRU eviction.
|
|
12
|
+
*
|
|
13
|
+
* In these tests, the L2 provider is also a fresh MemoryCacheProvider —
|
|
14
|
+
* but it's a *separate* instance from the inline L1 Map inside the
|
|
15
|
+
* primitive, so we can distinguish hits clearly via `provider.getCalls`.
|
|
16
|
+
*/
|
|
17
|
+
describe("$cache memory tier (L1)", () => {
|
|
18
|
+
it("serves repeated reads from L1 without hitting the provider", async () => {
|
|
19
|
+
let calls = 0;
|
|
20
|
+
class App {
|
|
21
|
+
cache = $cache({
|
|
22
|
+
ttl: [5, "minutes"],
|
|
23
|
+
memory: true,
|
|
24
|
+
handler: async (id: string) => {
|
|
25
|
+
calls++;
|
|
26
|
+
return `v:${id}`;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const alepha = Alepha.create().with({
|
|
32
|
+
provide: CacheProvider,
|
|
33
|
+
use: MemoryCacheProvider,
|
|
34
|
+
});
|
|
35
|
+
const app = alepha.inject(App);
|
|
36
|
+
const provider = alepha.inject(CacheProvider) as MemoryCacheProvider;
|
|
37
|
+
await alepha.start();
|
|
38
|
+
|
|
39
|
+
expect(await app.cache("a")).toBe("v:a");
|
|
40
|
+
const providerGetsAfterFirst = provider.getCalls.length;
|
|
41
|
+
expect(calls).toBe(1);
|
|
42
|
+
|
|
43
|
+
// Subsequent reads should hit L1 — no new provider gets.
|
|
44
|
+
expect(await app.cache("a")).toBe("v:a");
|
|
45
|
+
expect(await app.cache("a")).toBe("v:a");
|
|
46
|
+
expect(await app.cache("a")).toBe("v:a");
|
|
47
|
+
expect(provider.getCalls.length).toBe(providerGetsAfterFirst);
|
|
48
|
+
expect(calls).toBe(1);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("write-through populates both L1 and provider", async () => {
|
|
52
|
+
class App {
|
|
53
|
+
cache = $cache<string>({
|
|
54
|
+
ttl: [5, "minutes"],
|
|
55
|
+
memory: true,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const alepha = Alepha.create().with({
|
|
60
|
+
provide: CacheProvider,
|
|
61
|
+
use: MemoryCacheProvider,
|
|
62
|
+
});
|
|
63
|
+
const app = alepha.inject(App);
|
|
64
|
+
const provider = alepha.inject(CacheProvider) as MemoryCacheProvider;
|
|
65
|
+
await alepha.start();
|
|
66
|
+
|
|
67
|
+
await app.cache.set("k", "hello");
|
|
68
|
+
|
|
69
|
+
// Provider got the write
|
|
70
|
+
expect(provider.wasSet("App:cache", "k")).toBe(true);
|
|
71
|
+
|
|
72
|
+
// Subsequent read serves from L1 (no provider get call).
|
|
73
|
+
const getsBefore = provider.getCalls.length;
|
|
74
|
+
expect(await app.cache.get("k")).toBe("hello");
|
|
75
|
+
expect(provider.getCalls.length).toBe(getsBefore);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("falls back to provider when L1 expires", async () => {
|
|
79
|
+
let calls = 0;
|
|
80
|
+
class App {
|
|
81
|
+
cache = $cache({
|
|
82
|
+
ttl: [10, "minutes"],
|
|
83
|
+
memory: { ttl: [5, "seconds"] },
|
|
84
|
+
handler: async () => {
|
|
85
|
+
calls++;
|
|
86
|
+
return `v:${calls}`;
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const alepha = Alepha.create().with({
|
|
92
|
+
provide: CacheProvider,
|
|
93
|
+
use: MemoryCacheProvider,
|
|
94
|
+
});
|
|
95
|
+
const app = alepha.inject(App);
|
|
96
|
+
const provider = alepha.inject(CacheProvider) as MemoryCacheProvider;
|
|
97
|
+
const time = alepha.inject(DateTimeProvider);
|
|
98
|
+
await alepha.start();
|
|
99
|
+
|
|
100
|
+
expect(await app.cache()).toBe("v:1");
|
|
101
|
+
expect(calls).toBe(1);
|
|
102
|
+
|
|
103
|
+
// L1 still valid
|
|
104
|
+
await time.travel([3, "seconds"]);
|
|
105
|
+
const getsBeforeExpire = provider.getCalls.length;
|
|
106
|
+
expect(await app.cache()).toBe("v:1");
|
|
107
|
+
expect(provider.getCalls.length).toBe(getsBeforeExpire);
|
|
108
|
+
|
|
109
|
+
// L1 expired but L2 still valid — fallback to provider, no handler call
|
|
110
|
+
await time.travel([3, "seconds"]);
|
|
111
|
+
expect(await app.cache()).toBe("v:1");
|
|
112
|
+
expect(provider.getCalls.length).toBeGreaterThan(getsBeforeExpire);
|
|
113
|
+
expect(calls).toBe(1);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("uses default L1 TTL = min(ttl, 30s) when memory: true", async () => {
|
|
117
|
+
class App {
|
|
118
|
+
shortTtl = $cache<string>({
|
|
119
|
+
ttl: [5, "seconds"],
|
|
120
|
+
memory: true,
|
|
121
|
+
});
|
|
122
|
+
longTtl = $cache<string>({
|
|
123
|
+
ttl: [10, "minutes"],
|
|
124
|
+
memory: true,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const alepha = Alepha.create().with({
|
|
129
|
+
provide: CacheProvider,
|
|
130
|
+
use: MemoryCacheProvider,
|
|
131
|
+
});
|
|
132
|
+
const app = alepha.inject(App);
|
|
133
|
+
await alepha.start();
|
|
134
|
+
|
|
135
|
+
expect((app.shortTtl as any).memoryTtlMs).toBe(5_000);
|
|
136
|
+
expect((app.longTtl as any).memoryTtlMs).toBe(30_000);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("evicts oldest entries when L1 exceeds `max`", async () => {
|
|
140
|
+
class App {
|
|
141
|
+
cache = $cache<string>({
|
|
142
|
+
ttl: [5, "minutes"],
|
|
143
|
+
memory: { max: 3 },
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const alepha = Alepha.create().with({
|
|
148
|
+
provide: CacheProvider,
|
|
149
|
+
use: MemoryCacheProvider,
|
|
150
|
+
});
|
|
151
|
+
const app = alepha.inject(App);
|
|
152
|
+
const provider = alepha.inject(CacheProvider) as MemoryCacheProvider;
|
|
153
|
+
await alepha.start();
|
|
154
|
+
|
|
155
|
+
await app.cache.set("a", "A");
|
|
156
|
+
await app.cache.set("b", "B");
|
|
157
|
+
await app.cache.set("c", "C");
|
|
158
|
+
await app.cache.set("d", "D"); // should evict "a" from L1
|
|
159
|
+
|
|
160
|
+
// "a" reads should now miss L1 and hit provider
|
|
161
|
+
const getsBefore = provider.getCalls.length;
|
|
162
|
+
expect(await app.cache.get("a")).toBe("A"); // hits provider
|
|
163
|
+
expect(provider.getCalls.length).toBe(getsBefore + 1);
|
|
164
|
+
|
|
165
|
+
// "d" reads hit L1
|
|
166
|
+
const getsAfterA = provider.getCalls.length;
|
|
167
|
+
expect(await app.cache.get("d")).toBe("D");
|
|
168
|
+
expect(provider.getCalls.length).toBe(getsAfterA);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("LRU touch on read keeps recent entries", async () => {
|
|
172
|
+
class App {
|
|
173
|
+
cache = $cache<string>({
|
|
174
|
+
ttl: [5, "minutes"],
|
|
175
|
+
memory: { max: 3 },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const alepha = Alepha.create().with({
|
|
180
|
+
provide: CacheProvider,
|
|
181
|
+
use: MemoryCacheProvider,
|
|
182
|
+
});
|
|
183
|
+
const app = alepha.inject(App);
|
|
184
|
+
const provider = alepha.inject(CacheProvider) as MemoryCacheProvider;
|
|
185
|
+
await alepha.start();
|
|
186
|
+
|
|
187
|
+
await app.cache.set("a", "A");
|
|
188
|
+
await app.cache.set("b", "B");
|
|
189
|
+
await app.cache.set("c", "C");
|
|
190
|
+
|
|
191
|
+
// touch "a" -> moves it to end
|
|
192
|
+
expect(await app.cache.get("a")).toBe("A");
|
|
193
|
+
|
|
194
|
+
// now insert "d" -> should evict "b" (least recently used), not "a"
|
|
195
|
+
await app.cache.set("d", "D");
|
|
196
|
+
|
|
197
|
+
const getsBefore = provider.getCalls.length;
|
|
198
|
+
expect(await app.cache.get("a")).toBe("A"); // still in L1
|
|
199
|
+
expect(provider.getCalls.length).toBe(getsBefore);
|
|
200
|
+
|
|
201
|
+
expect(await app.cache.get("b")).toBe("B"); // miss L1, hit provider
|
|
202
|
+
expect(provider.getCalls.length).toBe(getsBefore + 1);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("caches provider misses with `negative` option", async () => {
|
|
206
|
+
class App {
|
|
207
|
+
cache = $cache<string>({
|
|
208
|
+
ttl: [5, "minutes"],
|
|
209
|
+
memory: { negative: [2, "seconds"] },
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const alepha = Alepha.create().with({
|
|
214
|
+
provide: CacheProvider,
|
|
215
|
+
use: MemoryCacheProvider,
|
|
216
|
+
});
|
|
217
|
+
const app = alepha.inject(App);
|
|
218
|
+
const provider = alepha.inject(CacheProvider) as MemoryCacheProvider;
|
|
219
|
+
const time = alepha.inject(DateTimeProvider);
|
|
220
|
+
await alepha.start();
|
|
221
|
+
|
|
222
|
+
expect(await app.cache.get("nope")).toBeUndefined();
|
|
223
|
+
const getsAfterFirst = provider.getCalls.length;
|
|
224
|
+
|
|
225
|
+
// Second miss read short-circuited via negative cache
|
|
226
|
+
expect(await app.cache.get("nope")).toBeUndefined();
|
|
227
|
+
expect(provider.getCalls.length).toBe(getsAfterFirst);
|
|
228
|
+
|
|
229
|
+
// After negative TTL expires, provider is consulted again
|
|
230
|
+
await time.travel([3, "seconds"]);
|
|
231
|
+
expect(await app.cache.get("nope")).toBeUndefined();
|
|
232
|
+
expect(provider.getCalls.length).toBeGreaterThan(getsAfterFirst);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("set() replaces a negative cache entry", async () => {
|
|
236
|
+
class App {
|
|
237
|
+
cache = $cache<string>({
|
|
238
|
+
ttl: [5, "minutes"],
|
|
239
|
+
memory: { negative: [10, "seconds"] },
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const alepha = Alepha.create().with({
|
|
244
|
+
provide: CacheProvider,
|
|
245
|
+
use: MemoryCacheProvider,
|
|
246
|
+
});
|
|
247
|
+
const app = alepha.inject(App);
|
|
248
|
+
await alepha.start();
|
|
249
|
+
|
|
250
|
+
expect(await app.cache.get("k")).toBeUndefined();
|
|
251
|
+
await app.cache.set("k", "value");
|
|
252
|
+
expect(await app.cache.get("k")).toBe("value");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("invalidate(key) clears both L1 and provider", async () => {
|
|
256
|
+
class App {
|
|
257
|
+
cache = $cache<string>({
|
|
258
|
+
ttl: [5, "minutes"],
|
|
259
|
+
memory: true,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const alepha = Alepha.create().with({
|
|
264
|
+
provide: CacheProvider,
|
|
265
|
+
use: MemoryCacheProvider,
|
|
266
|
+
});
|
|
267
|
+
const app = alepha.inject(App);
|
|
268
|
+
const provider = alepha.inject(CacheProvider) as MemoryCacheProvider;
|
|
269
|
+
await alepha.start();
|
|
270
|
+
|
|
271
|
+
await app.cache.set("k", "v");
|
|
272
|
+
await app.cache.invalidate("k");
|
|
273
|
+
|
|
274
|
+
expect(await provider.has("App:cache", "k")).toBe(false);
|
|
275
|
+
expect(await app.cache.get("k")).toBeUndefined();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("invalidate() with no keys clears all of L1", async () => {
|
|
279
|
+
class App {
|
|
280
|
+
cache = $cache<string>({
|
|
281
|
+
ttl: [5, "minutes"],
|
|
282
|
+
memory: true,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const alepha = Alepha.create().with({
|
|
287
|
+
provide: CacheProvider,
|
|
288
|
+
use: MemoryCacheProvider,
|
|
289
|
+
});
|
|
290
|
+
const app = alepha.inject(App);
|
|
291
|
+
await alepha.start();
|
|
292
|
+
|
|
293
|
+
await app.cache.set("a", "A");
|
|
294
|
+
await app.cache.set("b", "B");
|
|
295
|
+
await app.cache.invalidate();
|
|
296
|
+
|
|
297
|
+
expect(await app.cache.get("a")).toBeUndefined();
|
|
298
|
+
expect(await app.cache.get("b")).toBeUndefined();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("invalidate(prefix*) wildcard clears matching L1 entries", async () => {
|
|
302
|
+
class App {
|
|
303
|
+
cache = $cache<string>({
|
|
304
|
+
ttl: [5, "minutes"],
|
|
305
|
+
memory: true,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const alepha = Alepha.create().with({
|
|
310
|
+
provide: CacheProvider,
|
|
311
|
+
use: MemoryCacheProvider,
|
|
312
|
+
});
|
|
313
|
+
const app = alepha.inject(App);
|
|
314
|
+
await alepha.start();
|
|
315
|
+
|
|
316
|
+
await app.cache.set("user:1", "A");
|
|
317
|
+
await app.cache.set("user:2", "B");
|
|
318
|
+
await app.cache.set("post:1", "C");
|
|
319
|
+
|
|
320
|
+
await app.cache.invalidate("user:*");
|
|
321
|
+
|
|
322
|
+
expect(await app.cache.get("user:1")).toBeUndefined();
|
|
323
|
+
expect(await app.cache.get("user:2")).toBeUndefined();
|
|
324
|
+
expect(await app.cache.get("post:1")).toBe("C");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("incr() invalidates the L1 entry", async () => {
|
|
328
|
+
class App {
|
|
329
|
+
counter = $cache<number>({
|
|
330
|
+
ttl: [5, "minutes"],
|
|
331
|
+
memory: true,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const alepha = Alepha.create().with({
|
|
336
|
+
provide: CacheProvider,
|
|
337
|
+
use: MemoryCacheProvider,
|
|
338
|
+
});
|
|
339
|
+
const app = alepha.inject(App);
|
|
340
|
+
await alepha.start();
|
|
341
|
+
|
|
342
|
+
expect(await app.counter.incr("hits")).toBe(1);
|
|
343
|
+
// After incr, the provider has 1; L1 should be empty so the next get
|
|
344
|
+
// pulls the fresh value rather than an outdated L1 entry.
|
|
345
|
+
expect(await app.counter.get("hits")).toBe(1);
|
|
346
|
+
expect(await app.counter.incr("hits", 5)).toBe(6);
|
|
347
|
+
expect(await app.counter.get("hits")).toBe(6);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("works in middleware mode (pipeline use:[cache])", async () => {
|
|
351
|
+
class App {
|
|
352
|
+
calls = 0;
|
|
353
|
+
cache = $cache<string>({
|
|
354
|
+
name: "mw-mem",
|
|
355
|
+
ttl: [5, "minutes"],
|
|
356
|
+
memory: true,
|
|
357
|
+
});
|
|
358
|
+
fn = async (id: string) => {
|
|
359
|
+
const wrapped = this.cache(async (i: string) => {
|
|
360
|
+
this.calls++;
|
|
361
|
+
return `v:${i}`;
|
|
362
|
+
});
|
|
363
|
+
return wrapped(id);
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const alepha = Alepha.create().with({
|
|
368
|
+
provide: CacheProvider,
|
|
369
|
+
use: MemoryCacheProvider,
|
|
370
|
+
});
|
|
371
|
+
const app = alepha.inject(App);
|
|
372
|
+
const provider = alepha.inject(CacheProvider) as MemoryCacheProvider;
|
|
373
|
+
await alepha.start();
|
|
374
|
+
|
|
375
|
+
expect(await app.fn("a")).toBe("v:a");
|
|
376
|
+
const getsAfterFirst = provider.getCalls.length;
|
|
377
|
+
expect(await app.fn("a")).toBe("v:a");
|
|
378
|
+
expect(await app.fn("a")).toBe("v:a");
|
|
379
|
+
// L1 takes over — no new provider gets
|
|
380
|
+
expect(provider.getCalls.length).toBe(getsAfterFirst);
|
|
381
|
+
expect(app.calls).toBe(1);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("emits cache:hit on L1 read", async () => {
|
|
385
|
+
class App {
|
|
386
|
+
cache = $cache<string>({
|
|
387
|
+
ttl: [5, "minutes"],
|
|
388
|
+
memory: true,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const alepha = Alepha.create().with({
|
|
393
|
+
provide: CacheProvider,
|
|
394
|
+
use: MemoryCacheProvider,
|
|
395
|
+
});
|
|
396
|
+
const app = alepha.inject(App);
|
|
397
|
+
await alepha.start();
|
|
398
|
+
|
|
399
|
+
const hits: string[] = [];
|
|
400
|
+
alepha.events.on("cache:hit", (e) => {
|
|
401
|
+
hits.push(e.key);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
await app.cache.set("k", "v");
|
|
405
|
+
await app.cache.get("k");
|
|
406
|
+
await app.cache.get("k");
|
|
407
|
+
|
|
408
|
+
expect(hits).toContain("k");
|
|
409
|
+
expect(hits.length).toBeGreaterThanOrEqual(2);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("memory option off (undefined) does not allocate an L1 store", () => {
|
|
413
|
+
class App {
|
|
414
|
+
cache = $cache<string>({ ttl: [5, "minutes"] });
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const alepha = Alepha.create().with({
|
|
418
|
+
provide: CacheProvider,
|
|
419
|
+
use: MemoryCacheProvider,
|
|
420
|
+
});
|
|
421
|
+
const app = alepha.inject(App);
|
|
422
|
+
|
|
423
|
+
expect((app.cache as any).memoryStore).toBeUndefined();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("respects disabled option even with memory L1", async () => {
|
|
427
|
+
let calls = 0;
|
|
428
|
+
class App {
|
|
429
|
+
cache = $cache({
|
|
430
|
+
disabled: true,
|
|
431
|
+
memory: true,
|
|
432
|
+
handler: async () => {
|
|
433
|
+
calls++;
|
|
434
|
+
return `v:${calls}`;
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const alepha = Alepha.create().with({
|
|
440
|
+
provide: CacheProvider,
|
|
441
|
+
use: MemoryCacheProvider,
|
|
442
|
+
});
|
|
443
|
+
const app = alepha.inject(App);
|
|
444
|
+
await alepha.start();
|
|
445
|
+
|
|
446
|
+
expect(await app.cache()).toBe("v:1");
|
|
447
|
+
expect(await app.cache()).toBe("v:2");
|
|
448
|
+
expect(await app.cache()).toBe("v:3");
|
|
449
|
+
});
|
|
450
|
+
});
|