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
@@ -0,0 +1,394 @@
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 stale-while-revalidate (`stale` option).
8
+ *
9
+ * After `ttl` expires, the cached value remains servable for `stale`
10
+ * longer; reads in the grace window return the stale value immediately
11
+ * and trigger ONE background refresh (single-flight per key).
12
+ */
13
+ describe("$cache stale-while-revalidate (SWR)", () => {
14
+ it("returns fresh value without scheduling a refresh", async () => {
15
+ let calls = 0;
16
+ class App {
17
+ cache = $cache({
18
+ ttl: [5, "seconds"],
19
+ stale: [1, "hour"],
20
+ handler: async () => {
21
+ calls++;
22
+ return `v:${calls}`;
23
+ },
24
+ });
25
+ }
26
+
27
+ const alepha = Alepha.create().with({
28
+ provide: CacheProvider,
29
+ use: MemoryCacheProvider,
30
+ });
31
+ const app = alepha.inject(App);
32
+ const time = alepha.inject(DateTimeProvider);
33
+ await alepha.start();
34
+
35
+ expect(await app.cache()).toBe("v:1");
36
+ await time.travel([3, "seconds"]); // still fresh
37
+ expect(await app.cache()).toBe("v:1");
38
+ expect(await app.cache()).toBe("v:1");
39
+ expect(calls).toBe(1);
40
+ });
41
+
42
+ it("returns stale value and schedules a background refresh", async () => {
43
+ let calls = 0;
44
+ class App {
45
+ cache = $cache({
46
+ ttl: [5, "seconds"],
47
+ stale: [1, "hour"],
48
+ handler: async () => {
49
+ calls++;
50
+ return `v:${calls}`;
51
+ },
52
+ });
53
+ }
54
+
55
+ const alepha = Alepha.create().with({
56
+ provide: CacheProvider,
57
+ use: MemoryCacheProvider,
58
+ });
59
+ const app = alepha.inject(App);
60
+ const time = alepha.inject(DateTimeProvider);
61
+ await alepha.start();
62
+
63
+ expect(await app.cache()).toBe("v:1");
64
+ await time.travel([10, "seconds"]); // past ttl, within stale
65
+
66
+ // Stale value served immediately
67
+ expect(await app.cache()).toBe("v:1");
68
+
69
+ // Wait for background refresh to complete
70
+ await new Promise((r) => setTimeout(r, 20));
71
+
72
+ expect(calls).toBe(2);
73
+ // Next read returns the refreshed value
74
+ expect(await app.cache()).toBe("v:2");
75
+ });
76
+
77
+ it("single-flight: concurrent stale reads trigger one refresh", async () => {
78
+ let calls = 0;
79
+ class App {
80
+ cache = $cache({
81
+ ttl: [5, "seconds"],
82
+ stale: [1, "hour"],
83
+ handler: async () => {
84
+ calls++;
85
+ await new Promise((r) => setTimeout(r, 10));
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 time = alepha.inject(DateTimeProvider);
97
+ await alepha.start();
98
+
99
+ expect(await app.cache()).toBe("v:1");
100
+ await time.travel([10, "seconds"]);
101
+
102
+ // 10 concurrent reads
103
+ const results = await Promise.all(
104
+ Array.from({ length: 10 }, () => app.cache()),
105
+ );
106
+ expect(results.every((r) => r === "v:1")).toBe(true);
107
+
108
+ await new Promise((r) => setTimeout(r, 30));
109
+ expect(calls).toBe(2); // 1 original + 1 background refresh
110
+ });
111
+
112
+ it("single-flight: concurrent cold reads trigger one handler call", async () => {
113
+ let calls = 0;
114
+ class App {
115
+ cache = $cache({
116
+ ttl: [5, "seconds"],
117
+ stale: [1, "hour"],
118
+ handler: async () => {
119
+ calls++;
120
+ await new Promise((r) => setTimeout(r, 10));
121
+ return `v:${calls}`;
122
+ },
123
+ });
124
+ }
125
+
126
+ const alepha = Alepha.create().with({
127
+ provide: CacheProvider,
128
+ use: MemoryCacheProvider,
129
+ });
130
+ const app = alepha.inject(App);
131
+ await alepha.start();
132
+
133
+ const results = await Promise.all(
134
+ Array.from({ length: 10 }, () => app.cache()),
135
+ );
136
+ expect(results.every((r) => r === "v:1")).toBe(true);
137
+ expect(calls).toBe(1);
138
+ });
139
+
140
+ it("preserves stale value when background refresh fails", async () => {
141
+ let calls = 0;
142
+ class App {
143
+ cache = $cache({
144
+ ttl: [5, "seconds"],
145
+ stale: [1, "hour"],
146
+ handler: async () => {
147
+ calls++;
148
+ if (calls > 1) {
149
+ throw new Error("upstream down");
150
+ }
151
+ return `v:${calls}`;
152
+ },
153
+ });
154
+ }
155
+
156
+ const alepha = Alepha.create().with({
157
+ provide: CacheProvider,
158
+ use: MemoryCacheProvider,
159
+ });
160
+ const app = alepha.inject(App);
161
+ const time = alepha.inject(DateTimeProvider);
162
+ await alepha.start();
163
+
164
+ expect(await app.cache()).toBe("v:1");
165
+ await time.travel([10, "seconds"]);
166
+
167
+ // Stale read should succeed even though refresh fails in background.
168
+ expect(await app.cache()).toBe("v:1");
169
+
170
+ // Wait for failed refresh
171
+ await new Promise((r) => setTimeout(r, 30));
172
+
173
+ // Next read returns stale value again, attempts another refresh.
174
+ expect(await app.cache()).toBe("v:1");
175
+ });
176
+
177
+ it("treats values past ttl + stale as miss (synchronous handler call)", async () => {
178
+ let calls = 0;
179
+ class App {
180
+ cache = $cache({
181
+ ttl: [5, "seconds"],
182
+ stale: [10, "seconds"],
183
+ handler: async () => {
184
+ calls++;
185
+ return `v:${calls}`;
186
+ },
187
+ });
188
+ }
189
+
190
+ const alepha = Alepha.create().with({
191
+ provide: CacheProvider,
192
+ use: MemoryCacheProvider,
193
+ });
194
+ const app = alepha.inject(App);
195
+ const time = alepha.inject(DateTimeProvider);
196
+ await alepha.start();
197
+
198
+ expect(await app.cache()).toBe("v:1");
199
+
200
+ // Past ttl + stale -> provider expires the entry entirely
201
+ await time.travel([20, "seconds"]);
202
+
203
+ expect(await app.cache()).toBe("v:2");
204
+ expect(calls).toBe(2);
205
+ });
206
+
207
+ it("set() resets the fresh deadline and clears pending refresh", async () => {
208
+ let calls = 0;
209
+ class App {
210
+ cache = $cache<string>({
211
+ ttl: [5, "seconds"],
212
+ stale: [1, "hour"],
213
+ handler: () => {
214
+ calls++;
215
+ return `v:${calls}`;
216
+ },
217
+ });
218
+ }
219
+
220
+ const alepha = Alepha.create().with({
221
+ provide: CacheProvider,
222
+ use: MemoryCacheProvider,
223
+ });
224
+ const app = alepha.inject(App);
225
+ const time = alepha.inject(DateTimeProvider);
226
+ await alepha.start();
227
+
228
+ expect(await app.cache()).toBe("v:1");
229
+ await time.travel([10, "seconds"]); // stale
230
+
231
+ await app.cache.set(app.cache.key(), "manual");
232
+ // After manual set, fresh again -> no refresh needed
233
+ expect(await app.cache()).toBe("manual");
234
+ expect(await app.cache()).toBe("manual");
235
+ expect(calls).toBe(1); // handler never called for refresh
236
+ });
237
+
238
+ it("composes with L1 memory tier", async () => {
239
+ let calls = 0;
240
+ class App {
241
+ cache = $cache({
242
+ ttl: [10, "minutes"],
243
+ stale: [1, "hour"],
244
+ memory: { ttl: [5, "seconds"] },
245
+ handler: async () => {
246
+ calls++;
247
+ return `v:${calls}`;
248
+ },
249
+ });
250
+ }
251
+
252
+ const alepha = Alepha.create().with({
253
+ provide: CacheProvider,
254
+ use: MemoryCacheProvider,
255
+ });
256
+ const app = alepha.inject(App);
257
+ const provider = alepha.inject(CacheProvider) as MemoryCacheProvider;
258
+ await alepha.start();
259
+
260
+ expect(await app.cache()).toBe("v:1");
261
+ const getsAfterFirst = provider.getCalls.length;
262
+
263
+ // L1 serves the next reads.
264
+ expect(await app.cache()).toBe("v:1");
265
+ expect(await app.cache()).toBe("v:1");
266
+ expect(provider.getCalls.length).toBe(getsAfterFirst);
267
+ expect(calls).toBe(1);
268
+ });
269
+
270
+ it("works in middleware mode (handler from the pipeline)", async () => {
271
+ let calls = 0;
272
+ class App {
273
+ cache = $cache<string>({
274
+ name: "mw-swr",
275
+ ttl: [5, "seconds"],
276
+ stale: [1, "hour"],
277
+ });
278
+ fn = this.cache(async (id: string) => {
279
+ calls++;
280
+ return `v:${id}:${calls}`;
281
+ });
282
+ }
283
+
284
+ const alepha = Alepha.create().with({
285
+ provide: CacheProvider,
286
+ use: MemoryCacheProvider,
287
+ });
288
+ const app = alepha.inject(App);
289
+ const time = alepha.inject(DateTimeProvider);
290
+ await alepha.start();
291
+
292
+ expect(await app.fn("a")).toBe("v:a:1");
293
+ await time.travel([10, "seconds"]);
294
+
295
+ // Stale served + background refresh
296
+ expect(await app.fn("a")).toBe("v:a:1");
297
+ await new Promise((r) => setTimeout(r, 20));
298
+ expect(calls).toBe(2);
299
+ expect(await app.fn("a")).toBe("v:a:2");
300
+ });
301
+
302
+ it("emits cache:stale and cache:revalidate events", async () => {
303
+ let calls = 0;
304
+ class App {
305
+ cache = $cache({
306
+ ttl: [5, "seconds"],
307
+ stale: [1, "hour"],
308
+ handler: async () => {
309
+ calls++;
310
+ return `v:${calls}`;
311
+ },
312
+ });
313
+ }
314
+
315
+ const alepha = Alepha.create().with({
316
+ provide: CacheProvider,
317
+ use: MemoryCacheProvider,
318
+ });
319
+ const app = alepha.inject(App);
320
+ const time = alepha.inject(DateTimeProvider);
321
+ await alepha.start();
322
+
323
+ const staleEvents: string[] = [];
324
+ const revalidateEvents: string[] = [];
325
+ alepha.events.on("cache:stale", (e) => {
326
+ staleEvents.push(e.key);
327
+ });
328
+ alepha.events.on("cache:revalidate", (e) => {
329
+ revalidateEvents.push(e.key);
330
+ });
331
+
332
+ await app.cache();
333
+ await time.travel([10, "seconds"]);
334
+ await app.cache();
335
+
336
+ await new Promise((r) => setTimeout(r, 30));
337
+
338
+ expect(staleEvents.length).toBeGreaterThanOrEqual(1);
339
+ expect(revalidateEvents.length).toBeGreaterThanOrEqual(1);
340
+ });
341
+
342
+ it("manual mode (cache.get without handler) returns stale value but does not refresh", async () => {
343
+ class App {
344
+ cache = $cache<string>({
345
+ ttl: [5, "seconds"],
346
+ stale: [1, "hour"],
347
+ });
348
+ }
349
+
350
+ const alepha = Alepha.create().with({
351
+ provide: CacheProvider,
352
+ use: MemoryCacheProvider,
353
+ });
354
+ const app = alepha.inject(App);
355
+ const time = alepha.inject(DateTimeProvider);
356
+ await alepha.start();
357
+
358
+ await app.cache.set("k", "value");
359
+ await time.travel([10, "seconds"]);
360
+
361
+ // Stale served. No refresh possible (no handler).
362
+ expect(await app.cache.get("k")).toBe("value");
363
+ expect(await app.cache.get("k")).toBe("value");
364
+ });
365
+
366
+ it("stale option off behaves like a regular cache", async () => {
367
+ let calls = 0;
368
+ class App {
369
+ cache = $cache({
370
+ ttl: [5, "seconds"],
371
+ handler: async () => {
372
+ calls++;
373
+ return `v:${calls}`;
374
+ },
375
+ });
376
+ }
377
+
378
+ const alepha = Alepha.create().with({
379
+ provide: CacheProvider,
380
+ use: MemoryCacheProvider,
381
+ });
382
+ const app = alepha.inject(App);
383
+ const time = alepha.inject(DateTimeProvider);
384
+ await alepha.start();
385
+
386
+ expect(await app.cache()).toBe("v:1");
387
+ await time.travel([3, "seconds"]);
388
+ expect(await app.cache()).toBe("v:1");
389
+
390
+ // After ttl, no stale window -> immediate handler call
391
+ await time.travel([3, "seconds"]);
392
+ expect(await app.cache()).toBe("v:2");
393
+ });
394
+ });
@@ -36,6 +36,22 @@ declare module "alepha" {
36
36
  key: string;
37
37
  ttlMs?: number;
38
38
  };
39
+ /**
40
+ * Fires when a stale value (SWR grace window) is served and a
41
+ * background refresh is scheduled.
42
+ */
43
+ "cache:stale": {
44
+ container: string;
45
+ key: string;
46
+ };
47
+ /**
48
+ * Fires when a background SWR refresh completes successfully and
49
+ * the value has been written back to the cache.
50
+ */
51
+ "cache:revalidate": {
52
+ container: string;
53
+ key: string;
54
+ };
39
55
  }
40
56
  }
41
57