@databricks/appkit 0.0.2

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 (166) hide show
  1. package/CLAUDE.md +3 -0
  2. package/DCO +25 -0
  3. package/LICENSE +203 -0
  4. package/NOTICE.md +73 -0
  5. package/README.md +35 -0
  6. package/bin/setup-claude.js +190 -0
  7. package/dist/_virtual/rolldown_runtime.js +39 -0
  8. package/dist/analytics/analytics.d.ts +31 -0
  9. package/dist/analytics/analytics.d.ts.map +1 -0
  10. package/dist/analytics/analytics.js +149 -0
  11. package/dist/analytics/analytics.js.map +1 -0
  12. package/dist/analytics/defaults.js +17 -0
  13. package/dist/analytics/defaults.js.map +1 -0
  14. package/dist/analytics/index.js +3 -0
  15. package/dist/analytics/query.js +50 -0
  16. package/dist/analytics/query.js.map +1 -0
  17. package/dist/analytics/types.d.ts +9 -0
  18. package/dist/analytics/types.d.ts.map +1 -0
  19. package/dist/app/index.d.ts +23 -0
  20. package/dist/app/index.d.ts.map +1 -0
  21. package/dist/app/index.js +49 -0
  22. package/dist/app/index.js.map +1 -0
  23. package/dist/appkit/package.js +7 -0
  24. package/dist/appkit/package.js.map +1 -0
  25. package/dist/cache/defaults.js +14 -0
  26. package/dist/cache/defaults.js.map +1 -0
  27. package/dist/cache/index.d.ts +119 -0
  28. package/dist/cache/index.d.ts.map +1 -0
  29. package/dist/cache/index.js +307 -0
  30. package/dist/cache/index.js.map +1 -0
  31. package/dist/cache/storage/defaults.js +16 -0
  32. package/dist/cache/storage/defaults.js.map +1 -0
  33. package/dist/cache/storage/index.js +4 -0
  34. package/dist/cache/storage/memory.js +87 -0
  35. package/dist/cache/storage/memory.js.map +1 -0
  36. package/dist/cache/storage/persistent.js +211 -0
  37. package/dist/cache/storage/persistent.js.map +1 -0
  38. package/dist/connectors/index.js +6 -0
  39. package/dist/connectors/lakebase/client.js +348 -0
  40. package/dist/connectors/lakebase/client.js.map +1 -0
  41. package/dist/connectors/lakebase/defaults.js +13 -0
  42. package/dist/connectors/lakebase/defaults.js.map +1 -0
  43. package/dist/connectors/lakebase/index.js +3 -0
  44. package/dist/connectors/sql-warehouse/client.js +284 -0
  45. package/dist/connectors/sql-warehouse/client.js.map +1 -0
  46. package/dist/connectors/sql-warehouse/defaults.js +12 -0
  47. package/dist/connectors/sql-warehouse/defaults.js.map +1 -0
  48. package/dist/connectors/sql-warehouse/index.js +3 -0
  49. package/dist/core/appkit.d.ts +14 -0
  50. package/dist/core/appkit.d.ts.map +1 -0
  51. package/dist/core/appkit.js +66 -0
  52. package/dist/core/appkit.js.map +1 -0
  53. package/dist/core/index.js +3 -0
  54. package/dist/index.d.ts +15 -0
  55. package/dist/index.js +21 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/plugin/dev-reader.d.ts +20 -0
  58. package/dist/plugin/dev-reader.d.ts.map +1 -0
  59. package/dist/plugin/dev-reader.js +63 -0
  60. package/dist/plugin/dev-reader.js.map +1 -0
  61. package/dist/plugin/index.js +4 -0
  62. package/dist/plugin/interceptors/cache.js +15 -0
  63. package/dist/plugin/interceptors/cache.js.map +1 -0
  64. package/dist/plugin/interceptors/retry.js +32 -0
  65. package/dist/plugin/interceptors/retry.js.map +1 -0
  66. package/dist/plugin/interceptors/telemetry.js +33 -0
  67. package/dist/plugin/interceptors/telemetry.js.map +1 -0
  68. package/dist/plugin/interceptors/timeout.js +35 -0
  69. package/dist/plugin/interceptors/timeout.js.map +1 -0
  70. package/dist/plugin/plugin.d.ts +43 -0
  71. package/dist/plugin/plugin.d.ts.map +1 -0
  72. package/dist/plugin/plugin.js +119 -0
  73. package/dist/plugin/plugin.js.map +1 -0
  74. package/dist/plugin/to-plugin.d.ts +7 -0
  75. package/dist/plugin/to-plugin.d.ts.map +1 -0
  76. package/dist/plugin/to-plugin.js +12 -0
  77. package/dist/plugin/to-plugin.js.map +1 -0
  78. package/dist/server/base-server.js +24 -0
  79. package/dist/server/base-server.js.map +1 -0
  80. package/dist/server/index.d.ts +100 -0
  81. package/dist/server/index.d.ts.map +1 -0
  82. package/dist/server/index.js +224 -0
  83. package/dist/server/index.js.map +1 -0
  84. package/dist/server/remote-tunnel/denied.html +68 -0
  85. package/dist/server/remote-tunnel/gate.js +51 -0
  86. package/dist/server/remote-tunnel/gate.js.map +1 -0
  87. package/dist/server/remote-tunnel/index.html +165 -0
  88. package/dist/server/remote-tunnel/remote-tunnel-controller.js +100 -0
  89. package/dist/server/remote-tunnel/remote-tunnel-controller.js.map +1 -0
  90. package/dist/server/remote-tunnel/remote-tunnel-manager.js +320 -0
  91. package/dist/server/remote-tunnel/remote-tunnel-manager.js.map +1 -0
  92. package/dist/server/remote-tunnel/wait.html +158 -0
  93. package/dist/server/static-server.js +47 -0
  94. package/dist/server/static-server.js.map +1 -0
  95. package/dist/server/types.d.ts +14 -0
  96. package/dist/server/types.d.ts.map +1 -0
  97. package/dist/server/utils.js +70 -0
  98. package/dist/server/utils.js.map +1 -0
  99. package/dist/server/vite-dev-server.js +103 -0
  100. package/dist/server/vite-dev-server.js.map +1 -0
  101. package/dist/shared/src/cache.d.ts +62 -0
  102. package/dist/shared/src/cache.d.ts.map +1 -0
  103. package/dist/shared/src/execute.d.ts +46 -0
  104. package/dist/shared/src/execute.d.ts.map +1 -0
  105. package/dist/shared/src/plugin.d.ts +50 -0
  106. package/dist/shared/src/plugin.d.ts.map +1 -0
  107. package/dist/shared/src/sql/helpers.d.ts +160 -0
  108. package/dist/shared/src/sql/helpers.d.ts.map +1 -0
  109. package/dist/shared/src/sql/helpers.js +103 -0
  110. package/dist/shared/src/sql/helpers.js.map +1 -0
  111. package/dist/shared/src/sql/types.d.ts +34 -0
  112. package/dist/shared/src/sql/types.d.ts.map +1 -0
  113. package/dist/shared/src/tunnel.d.ts +30 -0
  114. package/dist/shared/src/tunnel.d.ts.map +1 -0
  115. package/dist/stream/arrow-stream-processor.js +154 -0
  116. package/dist/stream/arrow-stream-processor.js.map +1 -0
  117. package/dist/stream/buffers.js +88 -0
  118. package/dist/stream/buffers.js.map +1 -0
  119. package/dist/stream/defaults.js +14 -0
  120. package/dist/stream/defaults.js.map +1 -0
  121. package/dist/stream/index.js +6 -0
  122. package/dist/stream/sse-writer.js +61 -0
  123. package/dist/stream/sse-writer.js.map +1 -0
  124. package/dist/stream/stream-manager.d.ts +27 -0
  125. package/dist/stream/stream-manager.d.ts.map +1 -0
  126. package/dist/stream/stream-manager.js +191 -0
  127. package/dist/stream/stream-manager.js.map +1 -0
  128. package/dist/stream/stream-registry.js +54 -0
  129. package/dist/stream/stream-registry.js.map +1 -0
  130. package/dist/stream/types.js +14 -0
  131. package/dist/stream/types.js.map +1 -0
  132. package/dist/stream/validator.js +25 -0
  133. package/dist/stream/validator.js.map +1 -0
  134. package/dist/telemetry/config.js +20 -0
  135. package/dist/telemetry/config.js.map +1 -0
  136. package/dist/telemetry/index.d.ts +4 -0
  137. package/dist/telemetry/index.js +8 -0
  138. package/dist/telemetry/instrumentations.js +38 -0
  139. package/dist/telemetry/instrumentations.js.map +1 -0
  140. package/dist/telemetry/noop.js +54 -0
  141. package/dist/telemetry/noop.js.map +1 -0
  142. package/dist/telemetry/telemetry-manager.js +113 -0
  143. package/dist/telemetry/telemetry-manager.js.map +1 -0
  144. package/dist/telemetry/telemetry-provider.js +82 -0
  145. package/dist/telemetry/telemetry-provider.js.map +1 -0
  146. package/dist/telemetry/types.d.ts +74 -0
  147. package/dist/telemetry/types.d.ts.map +1 -0
  148. package/dist/type-generator/vite-plugin.d.ts +22 -0
  149. package/dist/type-generator/vite-plugin.d.ts.map +1 -0
  150. package/dist/type-generator/vite-plugin.js +49 -0
  151. package/dist/type-generator/vite-plugin.js.map +1 -0
  152. package/dist/utils/databricks-client-middleware.d.ts +17 -0
  153. package/dist/utils/databricks-client-middleware.d.ts.map +1 -0
  154. package/dist/utils/databricks-client-middleware.js +117 -0
  155. package/dist/utils/databricks-client-middleware.js.map +1 -0
  156. package/dist/utils/env-validator.js +14 -0
  157. package/dist/utils/env-validator.js.map +1 -0
  158. package/dist/utils/index.js +26 -0
  159. package/dist/utils/index.js.map +1 -0
  160. package/dist/utils/merge.js +25 -0
  161. package/dist/utils/merge.js.map +1 -0
  162. package/dist/utils/vite-config-merge.js +22 -0
  163. package/dist/utils/vite-config-merge.js.map +1 -0
  164. package/llms.txt +193 -0
  165. package/package.json +70 -0
  166. package/scripts/postinstall.js +6 -0
@@ -0,0 +1,307 @@
1
+ import { TelemetryManager } from "../telemetry/telemetry-manager.js";
2
+ import { SpanStatusCode } from "../telemetry/index.js";
3
+ import { deepMerge } from "../utils/merge.js";
4
+ import { init_utils } from "../utils/index.js";
5
+ import { LakebaseConnector } from "../connectors/lakebase/client.js";
6
+ import "../connectors/index.js";
7
+ import { cacheDefaults } from "./defaults.js";
8
+ import { InMemoryStorage } from "./storage/memory.js";
9
+ import { PersistentStorage } from "./storage/persistent.js";
10
+ import "./storage/index.js";
11
+ import { createHash } from "node:crypto";
12
+ import { WorkspaceClient } from "@databricks/sdk-experimental";
13
+
14
+ //#region src/cache/index.ts
15
+ init_utils();
16
+ /**
17
+ * Cache manager class to handle cache operations.
18
+ * Can be used with in-memory storage or persistent storage (Lakebase).
19
+ *
20
+ * The cache is automatically initialized by AppKit. Use `getInstanceSync()` to access
21
+ * the singleton instance after initialization.
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * const cache = CacheManager.getInstanceSync();
26
+ * const result = await cache.getOrExecute(["users", userId], () => fetchUser(userId), userKey);
27
+ * ```
28
+ */
29
+ var CacheManager = class CacheManager {
30
+ static {
31
+ this.MIN_CLEANUP_INTERVAL_MS = 6e4;
32
+ }
33
+ static {
34
+ this.instance = null;
35
+ }
36
+ static {
37
+ this.initPromise = null;
38
+ }
39
+ constructor(storage, config) {
40
+ this.name = "cache-manager";
41
+ this.storage = storage;
42
+ this.config = config;
43
+ this.inFlightRequests = /* @__PURE__ */ new Map();
44
+ this.cleanupInProgress = false;
45
+ this.lastCleanupAttempt = 0;
46
+ this.telemetry = TelemetryManager.getProvider(this.name, this.config.telemetry);
47
+ this.telemetryMetrics = {
48
+ cacheHitCount: this.telemetry.getMeter().createCounter("cache.hit", {
49
+ description: "Total number of cache hits",
50
+ unit: "1"
51
+ }),
52
+ cacheMissCount: this.telemetry.getMeter().createCounter("cache.miss", {
53
+ description: "Total number of cache misses",
54
+ unit: "1"
55
+ })
56
+ };
57
+ }
58
+ /**
59
+ * Get the singleton instance of the cache manager (sync version).
60
+ *
61
+ * Throws if not initialized - ensure AppKit.create() has completed first.
62
+ * @returns CacheManager instance
63
+ */
64
+ static getInstanceSync() {
65
+ if (!CacheManager.instance) throw new Error("CacheManager not initialized. Ensure AppKit.create() has completed before accessing the cache.");
66
+ return CacheManager.instance;
67
+ }
68
+ /**
69
+ * Initialize and get the singleton instance of the cache manager.
70
+ * Called internally by AppKit - prefer `getInstanceSync()` for plugin access.
71
+ * @param userConfig - User configuration for the cache manager
72
+ * @returns CacheManager instance
73
+ * @internal
74
+ */
75
+ static async getInstance(userConfig) {
76
+ if (CacheManager.instance) return CacheManager.instance;
77
+ if (!CacheManager.initPromise) CacheManager.initPromise = CacheManager.create(userConfig).then((instance) => {
78
+ CacheManager.instance = instance;
79
+ return instance;
80
+ });
81
+ return CacheManager.initPromise;
82
+ }
83
+ /**
84
+ * Create a new cache manager instance
85
+ *
86
+ * Storage selection logic:
87
+ * 1. If `storage` provided and healthy → use provided storage
88
+ * 2. If `storage` provided but unhealthy → fallback to InMemory (or disable if strictPersistence)
89
+ * 3. If no `storage` provided and Lakebase available → use Lakebase
90
+ * 4. If no `storage` provided and Lakebase unavailable → fallback to InMemory (or disable if strictPersistence)
91
+ *
92
+ * @param userConfig - User configuration for the cache manager
93
+ * @returns CacheManager instance
94
+ */
95
+ static async create(userConfig) {
96
+ const config = deepMerge(cacheDefaults, userConfig);
97
+ if (config.storage) {
98
+ if (await config.storage.healthCheck()) return new CacheManager(config.storage, config);
99
+ console.warn("[Cache] Provided storage health check failed");
100
+ if (config.strictPersistence) {
101
+ console.warn("[Cache] strictPersistence enabled but provided storage unhealthy. Cache disabled.");
102
+ const disabledConfig = {
103
+ ...config,
104
+ enabled: false
105
+ };
106
+ return new CacheManager(new InMemoryStorage(disabledConfig), disabledConfig);
107
+ }
108
+ console.warn("[Cache] Falling back to in-memory cache.");
109
+ return new CacheManager(new InMemoryStorage(config), config);
110
+ }
111
+ try {
112
+ const connector = new LakebaseConnector({ workspaceClient: new WorkspaceClient({}) });
113
+ if (await connector.healthCheck()) {
114
+ const persistentStorage = new PersistentStorage(config, connector);
115
+ await persistentStorage.initialize();
116
+ return new CacheManager(persistentStorage, config);
117
+ }
118
+ console.warn("[Cache] Lakebase health check failed, default storage unhealthy");
119
+ } catch (error) {
120
+ const errorMessage = error instanceof Error ? error.message : String(error);
121
+ console.warn(`[Cache] Lakebase unavailable: ${errorMessage}`);
122
+ }
123
+ if (config.strictPersistence) {
124
+ console.warn("[Cache] strictPersistence enabled but lakebase unavailable. Cache disabled.");
125
+ const disabledConfig = {
126
+ ...config,
127
+ enabled: false
128
+ };
129
+ return new CacheManager(new InMemoryStorage(disabledConfig), disabledConfig);
130
+ }
131
+ console.warn("[Cache] Falling back to in-memory cache.");
132
+ return new CacheManager(new InMemoryStorage(config), config);
133
+ }
134
+ /**
135
+ * Get or execute a function and cache the result
136
+ * @param key - Cache key
137
+ * @param fn - Function to execute
138
+ * @param userKey - User key
139
+ * @param options - Options for the cache
140
+ * @returns Promise of the result
141
+ */
142
+ async getOrExecute(key, fn, userKey, options) {
143
+ if (!this.config.enabled) return fn();
144
+ const cacheKey = this.generateKey(key, userKey);
145
+ return this.telemetry.startActiveSpan("cache.getOrExecute", { attributes: {
146
+ "cache.key": cacheKey,
147
+ "cache.enabled": this.config.enabled,
148
+ "cache.persistent": this.storage.isPersistent()
149
+ } }, async (span) => {
150
+ try {
151
+ const cached = await this.storage.get(cacheKey);
152
+ if (cached !== null) {
153
+ span.setAttribute("cache.hit", true);
154
+ span.setStatus({ code: SpanStatusCode.OK });
155
+ this.telemetryMetrics.cacheHitCount.add(1, { "cache.key": cacheKey });
156
+ return cached.value;
157
+ }
158
+ const inFlight = this.inFlightRequests.get(cacheKey);
159
+ if (inFlight) {
160
+ span.setAttribute("cache.hit", true);
161
+ span.setAttribute("cache.deduplication", true);
162
+ span.addEvent("cache.deduplication_used", { "cache.key": cacheKey });
163
+ span.setStatus({ code: SpanStatusCode.OK });
164
+ this.telemetryMetrics.cacheHitCount.add(1, {
165
+ "cache.key": cacheKey,
166
+ "cache.deduplication": "true"
167
+ });
168
+ span.end();
169
+ return inFlight;
170
+ }
171
+ span.setAttribute("cache.hit", false);
172
+ span.addEvent("cache.miss", { "cache.key": cacheKey });
173
+ this.telemetryMetrics.cacheMissCount.add(1, { "cache.key": cacheKey });
174
+ const promise = fn().then(async (result$1) => {
175
+ await this.set(cacheKey, result$1, options);
176
+ span.addEvent("cache.value_stored", {
177
+ "cache.key": cacheKey,
178
+ "cache.ttl": options?.ttl ?? this.config.ttl ?? 3600
179
+ });
180
+ return result$1;
181
+ }).catch((error) => {
182
+ span.recordException(error);
183
+ span.setStatus({ code: SpanStatusCode.ERROR });
184
+ throw error;
185
+ }).finally(() => {
186
+ this.inFlightRequests.delete(cacheKey);
187
+ });
188
+ this.inFlightRequests.set(cacheKey, promise);
189
+ const result = await promise;
190
+ span.setStatus({ code: SpanStatusCode.OK });
191
+ return result;
192
+ } catch (error) {
193
+ span.recordException(error);
194
+ span.setStatus({ code: SpanStatusCode.ERROR });
195
+ throw error;
196
+ } finally {
197
+ span.end();
198
+ }
199
+ }, {
200
+ name: this.name,
201
+ includePrefix: true
202
+ });
203
+ }
204
+ /**
205
+ * Get a cached value
206
+ * @param key - Cache key
207
+ * @returns Promise of the value or null if not found or expired
208
+ */
209
+ async get(key) {
210
+ if (!this.config.enabled) return null;
211
+ this.maybeCleanup();
212
+ const entry = await this.storage.get(key);
213
+ if (!entry) return null;
214
+ if (Date.now() > entry.expiry) {
215
+ await this.storage.delete(key);
216
+ return null;
217
+ }
218
+ return entry.value;
219
+ }
220
+ /** Probabilistically trigger cleanup of expired entries (fire-and-forget) */
221
+ maybeCleanup() {
222
+ if (this.cleanupInProgress) return;
223
+ if (!this.storage.isPersistent()) return;
224
+ const now = Date.now();
225
+ if (now - this.lastCleanupAttempt < CacheManager.MIN_CLEANUP_INTERVAL_MS) return;
226
+ const probability = this.config.cleanupProbability ?? .01;
227
+ if (Math.random() > probability) return;
228
+ this.lastCleanupAttempt = now;
229
+ this.cleanupInProgress = true;
230
+ this.storage.cleanupExpired().catch((error) => {
231
+ console.debug("Error cleaning up expired entries:", error);
232
+ }).finally(() => {
233
+ this.cleanupInProgress = false;
234
+ });
235
+ }
236
+ /**
237
+ * Set a value in the cache
238
+ * @param key - Cache key
239
+ * @param value - Value to set
240
+ * @param options - Options for the cache
241
+ * @returns Promise of the result
242
+ */
243
+ async set(key, value, options) {
244
+ if (!this.config.enabled) return;
245
+ const ttl = options?.ttl ?? this.config.ttl ?? 3600;
246
+ const expiryTime = Date.now() + ttl * 1e3;
247
+ await this.storage.set(key, {
248
+ value,
249
+ expiry: expiryTime
250
+ });
251
+ }
252
+ /**
253
+ * Delete a value from the cache
254
+ * @param key - Cache key
255
+ * @returns Promise of the result
256
+ */
257
+ async delete(key) {
258
+ if (!this.config.enabled) return;
259
+ await this.storage.delete(key);
260
+ }
261
+ /** Clear the cache */
262
+ async clear() {
263
+ await this.storage.clear();
264
+ this.inFlightRequests.clear();
265
+ }
266
+ /**
267
+ * Check if a value exists in the cache
268
+ * @param key - Cache key
269
+ * @returns Promise of true if the value exists, false otherwise
270
+ */
271
+ async has(key) {
272
+ if (!this.config.enabled) return false;
273
+ const entry = await this.storage.get(key);
274
+ if (!entry) return false;
275
+ if (Date.now() > entry.expiry) {
276
+ await this.storage.delete(key);
277
+ return false;
278
+ }
279
+ return true;
280
+ }
281
+ /**
282
+ * Generate a cache key
283
+ * @param parts - Parts of the key
284
+ * @param userKey - User key
285
+ * @returns Cache key
286
+ */
287
+ generateKey(parts, userKey) {
288
+ const allParts = [userKey, ...parts];
289
+ const serialized = JSON.stringify(allParts);
290
+ return createHash("sha256").update(serialized).digest("hex");
291
+ }
292
+ /** Close the cache */
293
+ async close() {
294
+ await this.storage.close();
295
+ }
296
+ /**
297
+ * Check if the storage is healthy
298
+ * @returns Promise of true if the storage is healthy, false otherwise
299
+ */
300
+ async isStorageHealthy() {
301
+ return this.storage.healthCheck();
302
+ }
303
+ };
304
+
305
+ //#endregion
306
+ export { CacheManager };
307
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["result"],"sources":["../../src/cache/index.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type { CacheConfig, CacheStorage } from \"shared\";\nimport { LakebaseConnector } from \"@/connectors\";\nimport type { Counter, TelemetryProvider } from \"../telemetry\";\nimport { SpanStatusCode, TelemetryManager } from \"../telemetry\";\nimport { deepMerge } from \"../utils\";\nimport { cacheDefaults } from \"./defaults\";\nimport { InMemoryStorage, PersistentStorage } from \"./storage\";\n\n/**\n * Cache manager class to handle cache operations.\n * Can be used with in-memory storage or persistent storage (Lakebase).\n *\n * The cache is automatically initialized by AppKit. Use `getInstanceSync()` to access\n * the singleton instance after initialization.\n *\n * @example\n * ```typescript\n * const cache = CacheManager.getInstanceSync();\n * const result = await cache.getOrExecute([\"users\", userId], () => fetchUser(userId), userKey);\n * ```\n */\nexport class CacheManager {\n private static readonly MIN_CLEANUP_INTERVAL_MS = 60_000;\n private readonly name: string = \"cache-manager\";\n private static instance: CacheManager | null = null;\n private static initPromise: Promise<CacheManager> | null = null;\n\n private storage: CacheStorage;\n private config: CacheConfig;\n private inFlightRequests: Map<string, Promise<unknown>>;\n private cleanupInProgress: boolean;\n private lastCleanupAttempt: number;\n\n // Telemetry\n private telemetry: TelemetryProvider;\n private telemetryMetrics: {\n cacheHitCount: Counter;\n cacheMissCount: Counter;\n };\n\n private constructor(storage: CacheStorage, config: CacheConfig) {\n this.storage = storage;\n this.config = config;\n this.inFlightRequests = new Map();\n this.cleanupInProgress = false;\n this.lastCleanupAttempt = 0;\n\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n this.config.telemetry,\n );\n this.telemetryMetrics = {\n cacheHitCount: this.telemetry.getMeter().createCounter(\"cache.hit\", {\n description: \"Total number of cache hits\",\n unit: \"1\",\n }),\n cacheMissCount: this.telemetry.getMeter().createCounter(\"cache.miss\", {\n description: \"Total number of cache misses\",\n unit: \"1\",\n }),\n };\n }\n\n /**\n * Get the singleton instance of the cache manager (sync version).\n *\n * Throws if not initialized - ensure AppKit.create() has completed first.\n * @returns CacheManager instance\n */\n static getInstanceSync(): CacheManager {\n if (!CacheManager.instance) {\n throw new Error(\n \"CacheManager not initialized. Ensure AppKit.create() has completed before accessing the cache.\",\n );\n }\n\n return CacheManager.instance;\n }\n\n /**\n * Initialize and get the singleton instance of the cache manager.\n * Called internally by AppKit - prefer `getInstanceSync()` for plugin access.\n * @param userConfig - User configuration for the cache manager\n * @returns CacheManager instance\n * @internal\n */\n static async getInstance(\n userConfig?: Partial<CacheConfig>,\n ): Promise<CacheManager> {\n if (CacheManager.instance) {\n return CacheManager.instance;\n }\n\n if (!CacheManager.initPromise) {\n CacheManager.initPromise = CacheManager.create(userConfig).then(\n (instance) => {\n CacheManager.instance = instance;\n return instance;\n },\n );\n }\n\n return CacheManager.initPromise;\n }\n\n /**\n * Create a new cache manager instance\n *\n * Storage selection logic:\n * 1. If `storage` provided and healthy → use provided storage\n * 2. If `storage` provided but unhealthy → fallback to InMemory (or disable if strictPersistence)\n * 3. If no `storage` provided and Lakebase available → use Lakebase\n * 4. If no `storage` provided and Lakebase unavailable → fallback to InMemory (or disable if strictPersistence)\n *\n * @param userConfig - User configuration for the cache manager\n * @returns CacheManager instance\n */\n private static async create(\n userConfig?: Partial<CacheConfig>,\n ): Promise<CacheManager> {\n const config = deepMerge(cacheDefaults, userConfig);\n\n if (config.storage) {\n const isHealthy = await config.storage.healthCheck();\n if (isHealthy) {\n return new CacheManager(config.storage, config);\n }\n\n console.warn(\"[Cache] Provided storage health check failed\");\n\n if (config.strictPersistence) {\n console.warn(\n \"[Cache] strictPersistence enabled but provided storage unhealthy. Cache disabled.\",\n );\n const disabledConfig = { ...config, enabled: false };\n return new CacheManager(\n new InMemoryStorage(disabledConfig),\n disabledConfig,\n );\n }\n\n console.warn(\"[Cache] Falling back to in-memory cache.\");\n return new CacheManager(new InMemoryStorage(config), config);\n }\n\n // try to use lakebase storage\n try {\n const workspaceClient = new WorkspaceClient({});\n const connector = new LakebaseConnector({ workspaceClient });\n const isHealthy = await connector.healthCheck();\n\n if (isHealthy) {\n const persistentStorage = new PersistentStorage(config, connector);\n await persistentStorage.initialize();\n return new CacheManager(persistentStorage, config);\n }\n\n console.warn(\n \"[Cache] Lakebase health check failed, default storage unhealthy\",\n );\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n console.warn(`[Cache] Lakebase unavailable: ${errorMessage}`);\n }\n\n if (config.strictPersistence) {\n console.warn(\n \"[Cache] strictPersistence enabled but lakebase unavailable. Cache disabled.\",\n );\n const disabledConfig = { ...config, enabled: false };\n return new CacheManager(\n new InMemoryStorage(disabledConfig),\n disabledConfig,\n );\n }\n\n console.warn(\"[Cache] Falling back to in-memory cache.\");\n return new CacheManager(new InMemoryStorage(config), config);\n }\n\n /**\n * Get or execute a function and cache the result\n * @param key - Cache key\n * @param fn - Function to execute\n * @param userKey - User key\n * @param options - Options for the cache\n * @returns Promise of the result\n */\n async getOrExecute<T>(\n key: (string | number | object)[],\n fn: () => Promise<T>,\n userKey: string,\n options?: { ttl?: number },\n ): Promise<T> {\n if (!this.config.enabled) return fn();\n\n const cacheKey = this.generateKey(key, userKey);\n\n return this.telemetry.startActiveSpan(\n \"cache.getOrExecute\",\n {\n attributes: {\n \"cache.key\": cacheKey,\n \"cache.enabled\": this.config.enabled,\n \"cache.persistent\": this.storage.isPersistent(),\n },\n },\n async (span) => {\n try {\n // check if the value is in the cache\n const cached = await this.storage.get<T>(cacheKey);\n if (cached !== null) {\n span.setAttribute(\"cache.hit\", true);\n span.setStatus({ code: SpanStatusCode.OK });\n this.telemetryMetrics.cacheHitCount.add(1, {\n \"cache.key\": cacheKey,\n });\n return cached.value as T;\n }\n\n // check if the value is being processed by another request\n const inFlight = this.inFlightRequests.get(cacheKey);\n if (inFlight) {\n span.setAttribute(\"cache.hit\", true);\n span.setAttribute(\"cache.deduplication\", true);\n span.addEvent(\"cache.deduplication_used\", {\n \"cache.key\": cacheKey,\n });\n span.setStatus({ code: SpanStatusCode.OK });\n this.telemetryMetrics.cacheHitCount.add(1, {\n \"cache.key\": cacheKey,\n \"cache.deduplication\": \"true\",\n });\n span.end();\n return inFlight as Promise<T>;\n }\n\n // cache miss - execute function\n span.setAttribute(\"cache.hit\", false);\n span.addEvent(\"cache.miss\", { \"cache.key\": cacheKey });\n this.telemetryMetrics.cacheMissCount.add(1, {\n \"cache.key\": cacheKey,\n });\n\n const promise = fn()\n .then(async (result) => {\n await this.set(cacheKey, result, options);\n span.addEvent(\"cache.value_stored\", {\n \"cache.key\": cacheKey,\n \"cache.ttl\": options?.ttl ?? this.config.ttl ?? 3600,\n });\n return result;\n })\n .catch((error) => {\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n })\n .finally(() => {\n this.inFlightRequests.delete(cacheKey);\n });\n\n this.inFlightRequests.set(cacheKey, promise);\n\n const result = await promise;\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n } finally {\n span.end();\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n /**\n * Get a cached value\n * @param key - Cache key\n * @returns Promise of the value or null if not found or expired\n */\n async get<T>(key: string): Promise<T | null> {\n if (!this.config.enabled) return null;\n\n // probabilistic cleanup trigger\n this.maybeCleanup();\n\n const entry = await this.storage.get<T>(key);\n if (!entry) return null;\n\n if (Date.now() > entry.expiry) {\n await this.storage.delete(key);\n return null;\n }\n return entry.value as T;\n }\n\n /** Probabilistically trigger cleanup of expired entries (fire-and-forget) */\n private maybeCleanup(): void {\n if (this.cleanupInProgress) return;\n if (!this.storage.isPersistent()) return;\n const now = Date.now();\n if (now - this.lastCleanupAttempt < CacheManager.MIN_CLEANUP_INTERVAL_MS)\n return;\n\n const probability = this.config.cleanupProbability ?? 0.01;\n\n if (Math.random() > probability) return;\n\n this.lastCleanupAttempt = now;\n\n this.cleanupInProgress = true;\n (this.storage as PersistentStorage)\n .cleanupExpired()\n .catch((error) => {\n console.debug(\"Error cleaning up expired entries:\", error);\n })\n .finally(() => {\n this.cleanupInProgress = false;\n });\n }\n\n /**\n * Set a value in the cache\n * @param key - Cache key\n * @param value - Value to set\n * @param options - Options for the cache\n * @returns Promise of the result\n */\n async set<T>(\n key: string,\n value: T,\n options?: { ttl?: number },\n ): Promise<void> {\n if (!this.config.enabled) return;\n\n const ttl = options?.ttl ?? this.config.ttl ?? 3600;\n const expiryTime = Date.now() + ttl * 1000;\n await this.storage.set(key, { value, expiry: expiryTime });\n }\n\n /**\n * Delete a value from the cache\n * @param key - Cache key\n * @returns Promise of the result\n */\n async delete(key: string): Promise<void> {\n if (!this.config.enabled) return;\n await this.storage.delete(key);\n }\n\n /** Clear the cache */\n async clear(): Promise<void> {\n await this.storage.clear();\n this.inFlightRequests.clear();\n }\n\n /**\n * Check if a value exists in the cache\n * @param key - Cache key\n * @returns Promise of true if the value exists, false otherwise\n */\n async has(key: string): Promise<boolean> {\n if (!this.config.enabled) return false;\n\n const entry = await this.storage.get(key);\n if (!entry) return false;\n\n if (Date.now() > entry.expiry) {\n await this.storage.delete(key);\n return false;\n }\n return true;\n }\n\n /**\n * Generate a cache key\n * @param parts - Parts of the key\n * @param userKey - User key\n * @returns Cache key\n */\n generateKey(parts: (string | number | object)[], userKey: string): string {\n const allParts = [userKey, ...parts];\n const serialized = JSON.stringify(allParts);\n return createHash(\"sha256\").update(serialized).digest(\"hex\");\n }\n\n /** Close the cache */\n async close(): Promise<void> {\n await this.storage.close();\n }\n\n /**\n * Check if the storage is healthy\n * @returns Promise of true if the storage is healthy, false otherwise\n */\n async isStorageHealthy(): Promise<boolean> {\n return this.storage.healthCheck();\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ //#region src/cache/storage/defaults.ts
2
+ /** Default configuration for in-memory storage */
3
+ const inMemoryStorageDefaults = { maxSize: 1e3 };
4
+ /** Default configuration for Lakebase storage */
5
+ const lakebaseStorageDefaults = {
6
+ tableName: "appkit_cache_entries",
7
+ maxBytes: 256 * 1024 * 1024,
8
+ maxEntryBytes: 10 * 1024 * 1024,
9
+ maxSize: 1e3,
10
+ evictionBatchSize: 100,
11
+ evictionCheckProbability: .1
12
+ };
13
+
14
+ //#endregion
15
+ export { inMemoryStorageDefaults, lakebaseStorageDefaults };
16
+ //# sourceMappingURL=defaults.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaults.js","names":[],"sources":["../../../src/cache/storage/defaults.ts"],"sourcesContent":["/** Default configuration for in-memory storage */\nexport const inMemoryStorageDefaults = {\n /** Maximum number of entries in the cache */\n maxSize: 1000,\n};\n\n/** Default configuration for Lakebase storage */\nexport const lakebaseStorageDefaults = {\n /** Table name for the cache */\n tableName: \"appkit_cache_entries\",\n /** Maximum number of bytes in the cache */\n maxBytes: 256 * 1024 * 1024, // 256MB\n /** Maximum number of bytes per entry in the cache */\n maxEntryBytes: 10 * 1024 * 1024, // 10MB\n /** Maximum number of entries in the cache */\n maxSize: 1000,\n /** Number of entries to evict when cache is full */\n evictionBatchSize: 100,\n /** Probability (0-1) of checking total bytes on each write operation */\n evictionCheckProbability: 0.1,\n};\n"],"mappings":";;AACA,MAAa,0BAA0B,EAErC,SAAS,KACV;;AAGD,MAAa,0BAA0B;CAErC,WAAW;CAEX,UAAU,MAAM,OAAO;CAEvB,eAAe,KAAK,OAAO;CAE3B,SAAS;CAET,mBAAmB;CAEnB,0BAA0B;CAC3B"}
@@ -0,0 +1,4 @@
1
+ import { InMemoryStorage } from "./memory.js";
2
+ import { PersistentStorage } from "./persistent.js";
3
+
4
+ export { };
@@ -0,0 +1,87 @@
1
+ import { inMemoryStorageDefaults } from "./defaults.js";
2
+
3
+ //#region src/cache/storage/memory.ts
4
+ /**
5
+ * In-memory cache storage implementation. Uses a least recently used (LRU) eviction policy
6
+ * to manage memory usage and ensure efficient cache operations.
7
+ */
8
+ var InMemoryStorage = class {
9
+ constructor(config) {
10
+ this.cache = /* @__PURE__ */ new Map();
11
+ this.accessOrder = /* @__PURE__ */ new Map();
12
+ this.cache = /* @__PURE__ */ new Map();
13
+ this.accessOrder = /* @__PURE__ */ new Map();
14
+ this.maxSize = config.maxSize ?? inMemoryStorageDefaults.maxSize;
15
+ this.accessCounter = 0;
16
+ }
17
+ /** Get an entry from the cache */
18
+ async get(key) {
19
+ const entry = this.cache.get(key);
20
+ if (!entry) return null;
21
+ this.accessOrder.set(key, ++this.accessCounter);
22
+ return entry;
23
+ }
24
+ /** Set an entry in the cache */
25
+ async set(key, entry) {
26
+ if (this.cache.size >= this.maxSize && !this.cache.has(key)) this.evictLRU();
27
+ this.cache.set(key, entry);
28
+ this.accessOrder.set(key, ++this.accessCounter);
29
+ }
30
+ /** Delete an entry from the cache */
31
+ async delete(key) {
32
+ this.cache.delete(key);
33
+ this.accessOrder.delete(key);
34
+ }
35
+ /** Clean in-memory cache */
36
+ async clear() {
37
+ this.cache.clear();
38
+ this.accessOrder.clear();
39
+ this.accessCounter = 0;
40
+ }
41
+ /** Check if the cache has an entry */
42
+ async has(key) {
43
+ const entry = this.cache.get(key);
44
+ if (!entry) return false;
45
+ if (Date.now() > entry.expiry) {
46
+ this.cache.delete(key);
47
+ this.accessOrder.delete(key);
48
+ return false;
49
+ }
50
+ return true;
51
+ }
52
+ /** Get the size of the cache */
53
+ async size() {
54
+ return this.cache.size;
55
+ }
56
+ /** Check if the cache is persistent */
57
+ isPersistent() {
58
+ return false;
59
+ }
60
+ /** Check the health of the cache */
61
+ async healthCheck() {
62
+ return true;
63
+ }
64
+ /** Close the cache */
65
+ async close() {
66
+ this.cache.clear();
67
+ this.accessOrder.clear();
68
+ this.accessCounter = 0;
69
+ }
70
+ /** Evict the least recently used entry (LRU) */
71
+ evictLRU() {
72
+ let oldestKey = null;
73
+ let oldestAccess = Infinity;
74
+ for (const [key, accessTime] of this.accessOrder) if (accessTime < oldestAccess) {
75
+ oldestAccess = accessTime;
76
+ oldestKey = key;
77
+ }
78
+ if (oldestKey) {
79
+ this.cache.delete(oldestKey);
80
+ this.accessOrder.delete(oldestKey);
81
+ }
82
+ }
83
+ };
84
+
85
+ //#endregion
86
+ export { InMemoryStorage };
87
+ //# sourceMappingURL=memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.js","names":["oldestKey: string | null"],"sources":["../../../src/cache/storage/memory.ts"],"sourcesContent":["import type { CacheConfig, CacheEntry, CacheStorage } from \"shared\";\nimport { inMemoryStorageDefaults } from \"./defaults\";\n\n/**\n * In-memory cache storage implementation. Uses a least recently used (LRU) eviction policy\n * to manage memory usage and ensure efficient cache operations.\n */\nexport class InMemoryStorage implements CacheStorage {\n private cache: Map<string, CacheEntry> = new Map();\n private accessOrder: Map<string, number> = new Map();\n private accessCounter: number;\n private maxSize: number;\n\n constructor(config: CacheConfig) {\n this.cache = new Map();\n this.accessOrder = new Map();\n this.maxSize = config.maxSize ?? inMemoryStorageDefaults.maxSize;\n this.accessCounter = 0;\n }\n\n /** Get an entry from the cache */\n async get<T>(key: string): Promise<CacheEntry<T> | null> {\n const entry = this.cache.get(key);\n if (!entry) return null;\n\n this.accessOrder.set(key, ++this.accessCounter);\n return entry as CacheEntry<T>;\n }\n\n /** Set an entry in the cache */\n async set<T>(key: string, entry: CacheEntry<T>): Promise<void> {\n if (this.cache.size >= this.maxSize && !this.cache.has(key)) {\n this.evictLRU();\n }\n\n this.cache.set(key, entry);\n this.accessOrder.set(key, ++this.accessCounter);\n }\n\n /** Delete an entry from the cache */\n async delete(key: string): Promise<void> {\n this.cache.delete(key);\n this.accessOrder.delete(key);\n }\n\n /** Clean in-memory cache */\n async clear(): Promise<void> {\n this.cache.clear();\n this.accessOrder.clear();\n this.accessCounter = 0;\n }\n\n /** Check if the cache has an entry */\n async has(key: string): Promise<boolean> {\n const entry = this.cache.get(key);\n if (!entry) return false;\n\n if (Date.now() > entry.expiry) {\n this.cache.delete(key);\n this.accessOrder.delete(key);\n return false;\n }\n\n return true;\n }\n\n /** Get the size of the cache */\n async size(): Promise<number> {\n return this.cache.size;\n }\n\n /** Check if the cache is persistent */\n isPersistent(): boolean {\n return false;\n }\n\n /** Check the health of the cache */\n async healthCheck(): Promise<boolean> {\n return true;\n }\n\n /** Close the cache */\n async close(): Promise<void> {\n this.cache.clear();\n this.accessOrder.clear();\n this.accessCounter = 0;\n }\n\n /** Evict the least recently used entry (LRU) */\n private evictLRU(): void {\n let oldestKey: string | null = null;\n let oldestAccess = Infinity;\n\n for (const [key, accessTime] of this.accessOrder) {\n if (accessTime < oldestAccess) {\n oldestAccess = accessTime;\n oldestKey = key;\n }\n }\n\n if (oldestKey) {\n this.cache.delete(oldestKey);\n this.accessOrder.delete(oldestKey);\n }\n }\n}\n"],"mappings":";;;;;;;AAOA,IAAa,kBAAb,MAAqD;CAMnD,YAAY,QAAqB;+BALQ,IAAI,KAAK;qCACP,IAAI,KAAK;AAKlD,OAAK,wBAAQ,IAAI,KAAK;AACtB,OAAK,8BAAc,IAAI,KAAK;AAC5B,OAAK,UAAU,OAAO,WAAW,wBAAwB;AACzD,OAAK,gBAAgB;;;CAIvB,MAAM,IAAO,KAA4C;EACvD,MAAM,QAAQ,KAAK,MAAM,IAAI,IAAI;AACjC,MAAI,CAAC,MAAO,QAAO;AAEnB,OAAK,YAAY,IAAI,KAAK,EAAE,KAAK,cAAc;AAC/C,SAAO;;;CAIT,MAAM,IAAO,KAAa,OAAqC;AAC7D,MAAI,KAAK,MAAM,QAAQ,KAAK,WAAW,CAAC,KAAK,MAAM,IAAI,IAAI,CACzD,MAAK,UAAU;AAGjB,OAAK,MAAM,IAAI,KAAK,MAAM;AAC1B,OAAK,YAAY,IAAI,KAAK,EAAE,KAAK,cAAc;;;CAIjD,MAAM,OAAO,KAA4B;AACvC,OAAK,MAAM,OAAO,IAAI;AACtB,OAAK,YAAY,OAAO,IAAI;;;CAI9B,MAAM,QAAuB;AAC3B,OAAK,MAAM,OAAO;AAClB,OAAK,YAAY,OAAO;AACxB,OAAK,gBAAgB;;;CAIvB,MAAM,IAAI,KAA+B;EACvC,MAAM,QAAQ,KAAK,MAAM,IAAI,IAAI;AACjC,MAAI,CAAC,MAAO,QAAO;AAEnB,MAAI,KAAK,KAAK,GAAG,MAAM,QAAQ;AAC7B,QAAK,MAAM,OAAO,IAAI;AACtB,QAAK,YAAY,OAAO,IAAI;AAC5B,UAAO;;AAGT,SAAO;;;CAIT,MAAM,OAAwB;AAC5B,SAAO,KAAK,MAAM;;;CAIpB,eAAwB;AACtB,SAAO;;;CAIT,MAAM,cAAgC;AACpC,SAAO;;;CAIT,MAAM,QAAuB;AAC3B,OAAK,MAAM,OAAO;AAClB,OAAK,YAAY,OAAO;AACxB,OAAK,gBAAgB;;;CAIvB,AAAQ,WAAiB;EACvB,IAAIA,YAA2B;EAC/B,IAAI,eAAe;AAEnB,OAAK,MAAM,CAAC,KAAK,eAAe,KAAK,YACnC,KAAI,aAAa,cAAc;AAC7B,kBAAe;AACf,eAAY;;AAIhB,MAAI,WAAW;AACb,QAAK,MAAM,OAAO,UAAU;AAC5B,QAAK,YAAY,OAAO,UAAU"}
@@ -0,0 +1,211 @@
1
+ import { lakebaseStorageDefaults } from "./defaults.js";
2
+ import { createHash } from "node:crypto";
3
+
4
+ //#region src/cache/storage/persistent.ts
5
+ /**
6
+ * Persistent cache storage implementation. Uses a least recently used (LRU) eviction policy
7
+ * to manage memory usage and ensure efficient cache operations.
8
+ *
9
+ * @example
10
+ * const persistentStorage = new PersistentStorage(config, connector);
11
+ * await persistentStorage.initialize();
12
+ * await persistentStorage.get("my-key");
13
+ * await persistentStorage.set("my-key", "my-value");
14
+ * await persistentStorage.delete("my-key");
15
+ * await persistentStorage.clear();
16
+ * await persistentStorage.has("my-key");
17
+ *
18
+ */
19
+ var PersistentStorage = class {
20
+ constructor(config, connector) {
21
+ this.connector = connector;
22
+ this.maxBytes = config.maxBytes ?? lakebaseStorageDefaults.maxBytes;
23
+ this.maxEntryBytes = config.maxEntryBytes ?? lakebaseStorageDefaults.maxEntryBytes;
24
+ this.evictionBatchSize = lakebaseStorageDefaults.evictionBatchSize;
25
+ this.evictionCheckProbability = config.evictionCheckProbability ?? lakebaseStorageDefaults.evictionCheckProbability;
26
+ this.tableName = lakebaseStorageDefaults.tableName;
27
+ this.initialized = false;
28
+ }
29
+ /** Initialize the persistent storage and run migrations if necessary */
30
+ async initialize() {
31
+ if (this.initialized) return;
32
+ try {
33
+ await this.runMigrations();
34
+ this.initialized = true;
35
+ } catch (error) {
36
+ console.error("Error in persistent storage initialization:", error);
37
+ throw error;
38
+ }
39
+ }
40
+ /**
41
+ * Get a cached value from the persistent storage
42
+ * @param key - Cache key
43
+ * @returns Promise of the cached value or null if not found
44
+ */
45
+ async get(key) {
46
+ await this.ensureInitialized();
47
+ const keyHash = this.hashKey(key);
48
+ const result = await this.connector.query(`SELECT value, expiry FROM ${this.tableName} WHERE key_hash = $1`, [keyHash]);
49
+ if (result.rows.length === 0) return null;
50
+ const entry = result.rows[0];
51
+ this.connector.query(`UPDATE ${this.tableName} SET last_accessed = NOW() WHERE key_hash = $1`, [keyHash]).catch(() => {
52
+ console.debug("Error updating last_accessed time for key:", key);
53
+ });
54
+ return {
55
+ value: this.deserializeValue(entry.value),
56
+ expiry: Number(entry.expiry)
57
+ };
58
+ }
59
+ /**
60
+ * Set a value in the persistent storage
61
+ * @param key - Cache key
62
+ * @param entry - Cache entry
63
+ * @returns Promise of the result
64
+ */
65
+ async set(key, entry) {
66
+ await this.ensureInitialized();
67
+ const keyHash = this.hashKey(key);
68
+ const keyBytes = Buffer.from(key, "utf-8");
69
+ const valueBytes = this.serializeValue(entry.value);
70
+ const byteSize = keyBytes.length + valueBytes.length;
71
+ if (byteSize > this.maxEntryBytes) throw new Error(`Cache entry too large: ${byteSize} bytes exceeds maximum of ${this.maxEntryBytes} bytes`);
72
+ if (Math.random() < this.evictionCheckProbability) {
73
+ if (await this.totalBytes() + byteSize > this.maxBytes) await this.evictBySize(byteSize);
74
+ }
75
+ await this.connector.query(`INSERT INTO ${this.tableName} (key_hash, key, value, byte_size, expiry, created_at, last_accessed)
76
+ VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
77
+ ON CONFLICT (key_hash)
78
+ DO UPDATE SET value = $3, byte_size = $4, expiry = $5, last_accessed = NOW()
79
+ `, [
80
+ keyHash,
81
+ keyBytes,
82
+ valueBytes,
83
+ byteSize,
84
+ entry.expiry
85
+ ]);
86
+ }
87
+ /**
88
+ * Delete a value from the persistent storage
89
+ * @param key - Cache key
90
+ * @returns Promise of the result
91
+ */
92
+ async delete(key) {
93
+ await this.ensureInitialized();
94
+ const keyHash = this.hashKey(key);
95
+ await this.connector.query(`DELETE FROM ${this.tableName} WHERE key_hash = $1`, [keyHash]);
96
+ }
97
+ /** Clear the persistent storage */
98
+ async clear() {
99
+ await this.ensureInitialized();
100
+ await this.connector.query(`TRUNCATE TABLE ${this.tableName}`);
101
+ }
102
+ /**
103
+ * Check if a value exists in the persistent storage
104
+ * @param key - Cache key
105
+ * @returns Promise of true if the value exists, false otherwise
106
+ */
107
+ async has(key) {
108
+ await this.ensureInitialized();
109
+ const keyHash = this.hashKey(key);
110
+ return (await this.connector.query(`SELECT EXISTS(SELECT 1 FROM ${this.tableName} WHERE key_hash = $1) as exists`, [keyHash])).rows[0]?.exists ?? false;
111
+ }
112
+ /**
113
+ * Get the size of the persistent storage
114
+ * @returns Promise of the size of the storage
115
+ */
116
+ async size() {
117
+ await this.ensureInitialized();
118
+ const result = await this.connector.query(`SELECT COUNT(*) as count FROM ${this.tableName}`);
119
+ return parseInt(result.rows[0]?.count ?? "0", 10);
120
+ }
121
+ /** Get the total number of bytes in the persistent storage */
122
+ async totalBytes() {
123
+ await this.ensureInitialized();
124
+ const result = await this.connector.query(`SELECT COALESCE(SUM(byte_size), 0) as total FROM ${this.tableName}`);
125
+ return parseInt(result.rows[0]?.total ?? "0", 10);
126
+ }
127
+ /**
128
+ * Check if the persistent storage is persistent
129
+ * @returns true if the storage is persistent, false otherwise
130
+ */
131
+ isPersistent() {
132
+ return true;
133
+ }
134
+ /**
135
+ * Check if the persistent storage is healthy
136
+ * @returns Promise of true if the storage is healthy, false otherwise
137
+ */
138
+ async healthCheck() {
139
+ try {
140
+ return await this.connector.healthCheck();
141
+ } catch {
142
+ return false;
143
+ }
144
+ }
145
+ /** Close the persistent storage */
146
+ async close() {
147
+ await this.connector.close();
148
+ }
149
+ /**
150
+ * Cleanup expired entries from the persistent storage
151
+ * @returns Promise of the number of expired entries
152
+ */
153
+ async cleanupExpired() {
154
+ await this.ensureInitialized();
155
+ const result = await this.connector.query(`WITH deleted as (DELETE FROM ${this.tableName} WHERE expiry < $1 RETURNING *) SELECT COUNT(*) as count FROM deleted`, [Date.now()]);
156
+ return parseInt(result.rows[0]?.count ?? "0", 10);
157
+ }
158
+ /** Evict entries from the persistent storage by size */
159
+ async evictBySize(requiredBytes) {
160
+ if (await this.cleanupExpired() > 0) {
161
+ if (await this.totalBytes() + requiredBytes <= this.maxBytes) return;
162
+ }
163
+ await this.connector.query(`DELETE FROM ${this.tableName} WHERE key_hash IN
164
+ (SELECT key_hash FROM ${this.tableName} ORDER BY last_accessed ASC LIMIT $1)`, [this.evictionBatchSize]);
165
+ }
166
+ /** Ensure the persistent storage is initialized */
167
+ async ensureInitialized() {
168
+ if (!this.initialized) await this.initialize();
169
+ }
170
+ /** Generate a 64-bit hash for the cache key using SHA256 */
171
+ hashKey(key) {
172
+ if (!key) throw new Error("Cache key cannot be empty");
173
+ return createHash("sha256").update(key).digest().readBigInt64BE(0);
174
+ }
175
+ /** Serialize a value to a buffer */
176
+ serializeValue(value) {
177
+ return Buffer.from(JSON.stringify(value), "utf-8");
178
+ }
179
+ /** Deserialize a value from a buffer */
180
+ deserializeValue(buffer) {
181
+ return JSON.parse(buffer.toString("utf-8"));
182
+ }
183
+ /** Run migrations for the persistent storage */
184
+ async runMigrations() {
185
+ try {
186
+ await this.connector.query(`
187
+ CREATE TABLE IF NOT EXISTS ${this.tableName} (
188
+ id BIGSERIAL PRIMARY KEY,
189
+ key_hash BIGINT NOT NULL,
190
+ key BYTEA NOT NULL,
191
+ value BYTEA NOT NULL,
192
+ byte_size INTEGER NOT NULL,
193
+ expiry BIGINT NOT NULL,
194
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
195
+ last_accessed TIMESTAMP NOT NULL DEFAULT NOW()
196
+ )
197
+ `);
198
+ await this.connector.query(`CREATE UNIQUE INDEX IF NOT EXISTS idx_${this.tableName}_key_hash ON ${this.tableName} (key_hash);`);
199
+ await this.connector.query(`CREATE INDEX IF NOT EXISTS idx_${this.tableName}_expiry ON ${this.tableName} (expiry); `);
200
+ await this.connector.query(`CREATE INDEX IF NOT EXISTS idx_${this.tableName}_last_accessed ON ${this.tableName} (last_accessed); `);
201
+ await this.connector.query(`CREATE INDEX IF NOT EXISTS idx_${this.tableName}_byte_size ON ${this.tableName} (byte_size); `);
202
+ } catch (error) {
203
+ console.error("Error in running migrations for persistent storage:", error);
204
+ throw error;
205
+ }
206
+ }
207
+ };
208
+
209
+ //#endregion
210
+ export { PersistentStorage };
211
+ //# sourceMappingURL=persistent.js.map