@dbx-tools/appkit 0.9.118 → 0.9.120

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.
@@ -0,0 +1,43 @@
1
+ /** Zod-owned operation contracts converted into native AppKit agent tools. */
2
+ import { defineTool } from "@databricks/appkit/beta";
3
+ /** Preserve operation inference while declaring an exported operation map. */
4
+ export function defineOperations(operations) {
5
+ return operations;
6
+ }
7
+ /** Convert exported Zod operation contracts into a native AppKit tool registry. */
8
+ export function toolRegistryFromOperations(operations) {
9
+ return Object.fromEntries(Object.entries(operations).flatMap(([name, operation]) => {
10
+ const metadata = operation.schema.meta();
11
+ const tool = metadata?.appkitTool ?? {};
12
+ if (tool.agentTool !== true)
13
+ return [];
14
+ const effect = tool.effect ?? "write";
15
+ const approval = tool.approval === true || effect === "destructive";
16
+ const description = operation.schema.description?.trim();
17
+ if (!description) {
18
+ throw new TypeError(`AppKit operation ${name} requires a Zod schema description`);
19
+ }
20
+ return [
21
+ [
22
+ name,
23
+ defineTool({
24
+ description,
25
+ schema: operation.schema,
26
+ annotations: {
27
+ effect,
28
+ ...(approval ? { destructive: true } : {}),
29
+ ...(tool.requiresUserContext === undefined
30
+ ? {}
31
+ : { requiresUserContext: tool.requiresUserContext }),
32
+ ...(tool.idempotent === undefined ? {} : { idempotent: tool.idempotent }),
33
+ },
34
+ ...(tool.autoInheritable === undefined
35
+ ? {}
36
+ : { autoInheritable: tool.autoInheritable }),
37
+ execute: (input, signal) => operation.execute(operation.schema.parse(input), signal),
38
+ }),
39
+ ],
40
+ ];
41
+ }));
42
+ }
43
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidG9vbC1vcGVyYXRpb25zLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3Rvb2wtb3BlcmF0aW9ucy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSw4RUFBOEU7QUFFOUUsT0FBTyxFQUFFLFVBQVUsRUFBcUIsTUFBTSx5QkFBeUIsQ0FBQztBQThCeEUsOEVBQThFO0FBQzlFLE1BQU0sVUFBVSxnQkFBZ0IsQ0FFOUIsVUFFRDtJQUNDLE9BQU8sVUFBVSxDQUFDO0FBQ3BCLENBQUM7QUFFRCxtRkFBbUY7QUFDbkYsTUFBTSxVQUFVLDBCQUEwQixDQUN4QyxVQUF5RTtJQUV6RSxPQUFPLE1BQU0sQ0FBQyxXQUFXLENBQ3ZCLE1BQU0sQ0FBQyxPQUFPLENBQUMsVUFBVSxDQUFDLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxJQUFJLEVBQUUsU0FBUyxDQUFDLEVBQUUsRUFBRTtRQUN2RCxNQUFNLFFBQVEsR0FBRyxTQUFTLENBQUMsTUFBTSxDQUFDLElBQUksRUFBMkMsQ0FBQztRQUNsRixNQUFNLElBQUksR0FBRyxRQUFRLEVBQUUsVUFBVSxJQUFJLEVBQUUsQ0FBQztRQUN4QyxJQUFJLElBQUksQ0FBQyxTQUFTLEtBQUssSUFBSTtZQUFFLE9BQU8sRUFBRSxDQUFDO1FBQ3ZDLE1BQU0sTUFBTSxHQUFHLElBQUksQ0FBQyxNQUFNLElBQUksT0FBTyxDQUFDO1FBQ3RDLE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxRQUFRLEtBQUssSUFBSSxJQUFJLE1BQU0sS0FBSyxhQUFhLENBQUM7UUFDcEUsTUFBTSxXQUFXLEdBQUcsU0FBUyxDQUFDLE1BQU0sQ0FBQyxXQUFXLEVBQUUsSUFBSSxFQUFFLENBQUM7UUFDekQsSUFBSSxDQUFDLFdBQVcsRUFBRSxDQUFDO1lBQ2pCLE1BQU0sSUFBSSxTQUFTLENBQUMsb0JBQW9CLElBQUksb0NBQW9DLENBQUMsQ0FBQztRQUNwRixDQUFDO1FBQ0QsT0FBTztZQUNMO2dCQUNFLElBQUk7Z0JBQ0osVUFBVSxDQUFDO29CQUNULFdBQVc7b0JBQ1gsTUFBTSxFQUFFLFNBQVMsQ0FBQyxNQUFNO29CQUN4QixXQUFXLEVBQUU7d0JBQ1gsTUFBTTt3QkFDTixHQUFHLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxFQUFFLFdBQVcsRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO3dCQUMxQyxHQUFHLENBQUMsSUFBSSxDQUFDLG1CQUFtQixLQUFLLFNBQVM7NEJBQ3hDLENBQUMsQ0FBQyxFQUFFOzRCQUNKLENBQUMsQ0FBQyxFQUFFLG1CQUFtQixFQUFFLElBQUksQ0FBQyxtQkFBbUIsRUFBRSxDQUFDO3dCQUN0RCxHQUFHLENBQUMsSUFBSSxDQUFDLFVBQVUsS0FBSyxTQUFTLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxVQUFVLEVBQUUsSUFBSSxDQUFDLFVBQVUsRUFBRSxDQUFDO3FCQUMxRTtvQkFDRCxHQUFHLENBQUMsSUFBSSxDQUFDLGVBQWUsS0FBSyxTQUFTO3dCQUNwQyxDQUFDLENBQUMsRUFBRTt3QkFDSixDQUFDLENBQUMsRUFBRSxlQUFlLEVBQUUsSUFBSSxDQUFDLGVBQWUsRUFBRSxDQUFDO29CQUM5QyxPQUFPLEVBQUUsQ0FBQyxLQUFLLEVBQUUsTUFBTSxFQUFFLEVBQUUsQ0FBQyxTQUFTLENBQUMsT0FBTyxDQUFDLFNBQVMsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLEtBQUssQ0FBQyxFQUFFLE1BQU0sQ0FBQztpQkFDckYsQ0FBQzthQUNIO1NBQ0YsQ0FBQztJQUNKLENBQUMsQ0FBQyxDQUNILENBQUM7QUFDSixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqIFpvZC1vd25lZCBvcGVyYXRpb24gY29udHJhY3RzIGNvbnZlcnRlZCBpbnRvIG5hdGl2ZSBBcHBLaXQgYWdlbnQgdG9vbHMuICovXG5cbmltcG9ydCB7IGRlZmluZVRvb2wsIHR5cGUgVG9vbFJlZ2lzdHJ5IH0gZnJvbSBcIkBkYXRhYnJpY2tzL2FwcGtpdC9iZXRhXCI7XG5pbXBvcnQgeyB6IH0gZnJvbSBcInpvZFwiO1xuXG4vKiogTWV0YWRhdGEgc3RvcmVkIHVuZGVyIGBhcHBraXRUb29sYCBieSBhIFpvZCBzY2hlbWEncyBgLm1ldGEoKWAgdmFsdWUuICovXG5leHBvcnQgaW50ZXJmYWNlIEFwcEtpdFRvb2xNZXRhIHtcbiAgLyoqIE9wdCBpbiB0byBleHBvc2luZyB0aGlzIG9wZXJhdGlvbiBhcyBhbiBhZ2VudCB0b29sLiAqL1xuICByZWFkb25seSBhZ2VudFRvb2w/OiBib29sZWFuO1xuICAvKiogVG9vbCBzaWRlLWVmZmVjdCBjbGFzc2lmaWNhdGlvbi4gRGVmYXVsdHMgdG8gYHdyaXRlYC4gKi9cbiAgcmVhZG9ubHkgZWZmZWN0PzogXCJyZWFkXCIgfCBcIndyaXRlXCIgfCBcImRlc3RydWN0aXZlXCI7XG4gIC8qKiBSZXF1aXJlIGh1bWFuIGFwcHJvdmFsIGJlZm9yZSBleGVjdXRpb24uICovXG4gIHJlYWRvbmx5IGFwcHJvdmFsPzogYm9vbGVhbjtcbiAgLyoqIFdoZXRoZXIgZXhlY3V0aW9uIHJlcXVpcmVzIGFuIGF1dGhlbnRpY2F0ZWQgdXNlciBjb250ZXh0LiAqL1xuICByZWFkb25seSByZXF1aXJlc1VzZXJDb250ZXh0PzogYm9vbGVhbjtcbiAgLyoqIFdoZXRoZXIgcmVwZWF0ZWQgY2FsbHMgd2l0aCB0aGUgc2FtZSBpbnB1dCBhcmUgc2FmZS4gKi9cbiAgcmVhZG9ubHkgaWRlbXBvdGVudD86IGJvb2xlYW47XG4gIC8qKiBXaGV0aGVyIEFwcEtpdCBtYXkgaW5oZXJpdCB0aGUgdG9vbCBhdXRvbWF0aWNhbGx5LiAqL1xuICByZWFkb25seSBhdXRvSW5oZXJpdGFibGU/OiBib29sZWFuO1xufVxuXG4vKiogU2hhcGUgcGxhY2VkIGluIGEgWm9kIHNjaGVtYSdzIGAubWV0YSgpYCB2YWx1ZS4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgQXBwS2l0T3BlcmF0aW9uU2NoZW1hTWV0YSB7XG4gIHJlYWRvbmx5IGFwcGtpdFRvb2w/OiBBcHBLaXRUb29sTWV0YTtcbn1cblxuLyoqIE9uZSB0eXBlZCBvcGVyYXRpb24gYmFja2VkIGJ5IGEgc2luZ2xlIFpvZCBpbnB1dCBjb250cmFjdC4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgQXBwS2l0T3BlcmF0aW9uPFRTY2hlbWEgZXh0ZW5kcyB6LlpvZFR5cGUgPSB6LlpvZFR5cGUsIFRSZXN1bHQgPSB1bmtub3duPiB7XG4gIHJlYWRvbmx5IHNjaGVtYTogVFNjaGVtYTtcbiAgcmVhZG9ubHkgZXhlY3V0ZTogKGlucHV0OiB6Lm91dHB1dDxUU2NoZW1hPiwgc2lnbmFsPzogQWJvcnRTaWduYWwpID0+IFRSZXN1bHQgfCBQcm9taXNlPFRSZXN1bHQ+O1xufVxuXG4vKiogUHJlc2VydmUgb3BlcmF0aW9uIGluZmVyZW5jZSB3aGlsZSBkZWNsYXJpbmcgYW4gZXhwb3J0ZWQgb3BlcmF0aW9uIG1hcC4gKi9cbmV4cG9ydCBmdW5jdGlvbiBkZWZpbmVPcGVyYXRpb25zPFxuICBjb25zdCBUU2NoZW1hcyBleHRlbmRzIFJlYWRvbmx5PFJlY29yZDxzdHJpbmcsIHouWm9kVHlwZT4+LFxuPihvcGVyYXRpb25zOiB7XG4gIHJlYWRvbmx5IFtUTmFtZSBpbiBrZXlvZiBUU2NoZW1hc106IEFwcEtpdE9wZXJhdGlvbjxUU2NoZW1hc1tUTmFtZV0sIHVua25vd24+O1xufSk6IHR5cGVvZiBvcGVyYXRpb25zIHtcbiAgcmV0dXJuIG9wZXJhdGlvbnM7XG59XG5cbi8qKiBDb252ZXJ0IGV4cG9ydGVkIFpvZCBvcGVyYXRpb24gY29udHJhY3RzIGludG8gYSBuYXRpdmUgQXBwS2l0IHRvb2wgcmVnaXN0cnkuICovXG5leHBvcnQgZnVuY3Rpb24gdG9vbFJlZ2lzdHJ5RnJvbU9wZXJhdGlvbnMoXG4gIG9wZXJhdGlvbnM6IFJlYWRvbmx5PFJlY29yZDxzdHJpbmcsIEFwcEtpdE9wZXJhdGlvbjx6LlpvZFR5cGUsIHVua25vd24+Pj4sXG4pOiBUb29sUmVnaXN0cnkge1xuICByZXR1cm4gT2JqZWN0LmZyb21FbnRyaWVzKFxuICAgIE9iamVjdC5lbnRyaWVzKG9wZXJhdGlvbnMpLmZsYXRNYXAoKFtuYW1lLCBvcGVyYXRpb25dKSA9PiB7XG4gICAgICBjb25zdCBtZXRhZGF0YSA9IG9wZXJhdGlvbi5zY2hlbWEubWV0YSgpIGFzIEFwcEtpdE9wZXJhdGlvblNjaGVtYU1ldGEgfCB1bmRlZmluZWQ7XG4gICAgICBjb25zdCB0b29sID0gbWV0YWRhdGE/LmFwcGtpdFRvb2wgPz8ge307XG4gICAgICBpZiAodG9vbC5hZ2VudFRvb2wgIT09IHRydWUpIHJldHVybiBbXTtcbiAgICAgIGNvbnN0IGVmZmVjdCA9IHRvb2wuZWZmZWN0ID8/IFwid3JpdGVcIjtcbiAgICAgIGNvbnN0IGFwcHJvdmFsID0gdG9vbC5hcHByb3ZhbCA9PT0gdHJ1ZSB8fCBlZmZlY3QgPT09IFwiZGVzdHJ1Y3RpdmVcIjtcbiAgICAgIGNvbnN0IGRlc2NyaXB0aW9uID0gb3BlcmF0aW9uLnNjaGVtYS5kZXNjcmlwdGlvbj8udHJpbSgpO1xuICAgICAgaWYgKCFkZXNjcmlwdGlvbikge1xuICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKGBBcHBLaXQgb3BlcmF0aW9uICR7bmFtZX0gcmVxdWlyZXMgYSBab2Qgc2NoZW1hIGRlc2NyaXB0aW9uYCk7XG4gICAgICB9XG4gICAgICByZXR1cm4gW1xuICAgICAgICBbXG4gICAgICAgICAgbmFtZSxcbiAgICAgICAgICBkZWZpbmVUb29sKHtcbiAgICAgICAgICAgIGRlc2NyaXB0aW9uLFxuICAgICAgICAgICAgc2NoZW1hOiBvcGVyYXRpb24uc2NoZW1hLFxuICAgICAgICAgICAgYW5ub3RhdGlvbnM6IHtcbiAgICAgICAgICAgICAgZWZmZWN0LFxuICAgICAgICAgICAgICAuLi4oYXBwcm92YWwgPyB7IGRlc3RydWN0aXZlOiB0cnVlIH0gOiB7fSksXG4gICAgICAgICAgICAgIC4uLih0b29sLnJlcXVpcmVzVXNlckNvbnRleHQgPT09IHVuZGVmaW5lZFxuICAgICAgICAgICAgICAgID8ge31cbiAgICAgICAgICAgICAgICA6IHsgcmVxdWlyZXNVc2VyQ29udGV4dDogdG9vbC5yZXF1aXJlc1VzZXJDb250ZXh0IH0pLFxuICAgICAgICAgICAgICAuLi4odG9vbC5pZGVtcG90ZW50ID09PSB1bmRlZmluZWQgPyB7fSA6IHsgaWRlbXBvdGVudDogdG9vbC5pZGVtcG90ZW50IH0pLFxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIC4uLih0b29sLmF1dG9Jbmhlcml0YWJsZSA9PT0gdW5kZWZpbmVkXG4gICAgICAgICAgICAgID8ge31cbiAgICAgICAgICAgICAgOiB7IGF1dG9Jbmhlcml0YWJsZTogdG9vbC5hdXRvSW5oZXJpdGFibGUgfSksXG4gICAgICAgICAgICBleGVjdXRlOiAoaW5wdXQsIHNpZ25hbCkgPT4gb3BlcmF0aW9uLmV4ZWN1dGUob3BlcmF0aW9uLnNjaGVtYS5wYXJzZShpbnB1dCksIHNpZ25hbCksXG4gICAgICAgICAgfSksXG4gICAgICAgIF0sXG4gICAgICBdO1xuICAgIH0pLFxuICApO1xufVxuIl19
@@ -1 +1 @@
1
- {"root":["../src/_auto-configure.ts","../src/_cache-storage.ts","../src/appkit.ts","../src/brand-context.ts","../src/bundle.ts","../src/child-process.ts","../src/databricks.ts","../src/files-cache.ts","../src/identity.ts","../src/interceptor.ts","../src/lakebase-resolver.ts","../src/migration.ts","../src/pgaddress.ts","../src/plugin-execution.ts","../src/plugin-registry.ts","../src/provision.ts","../src/request-tags.ts","../src/tool-provider.ts","../src/toolkit-entries.ts","../index.ts"],"version":"5.9.3"}
1
+ {"root":["../src/_auto-configure.ts","../src/appkit.ts","../src/brand-context.ts","../src/bundle.ts","../src/cache-storage.ts","../src/child-process.ts","../src/databricks.ts","../src/files-cache.ts","../src/identity.ts","../src/interceptor.ts","../src/lakebase-resolver.ts","../src/migration.ts","../src/pgaddress.ts","../src/plugin-execution.ts","../src/plugin-registry.ts","../src/provision.ts","../src/request-tags.ts","../src/tool-operations.ts","../src/tool-provider.ts","../src/toolkit-entries.ts","../index.ts"],"version":"5.9.3"}
package/package.json CHANGED
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "devDependencies": {
22
22
  "@databricks/appkit": "0.81.0",
23
- "@dbx-tools/projen": "^0.9.118",
23
+ "@dbx-tools/projen": "^0.9.120",
24
24
  "@types/node": "^24.6.0",
25
25
  "typescript": "^5.9.3",
26
26
  "vitest": "3.2.4"
@@ -30,13 +30,13 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@databricks/sdk-experimental": "^0.17.0",
33
- "@dbx-tools/auth": "^0.9.118",
34
- "@dbx-tools/core": "^0.9.118",
35
- "@dbx-tools/lakebase": "^0.9.118",
36
- "@dbx-tools/postgres": "^0.9.118",
37
- "@dbx-tools/shared-core": "^0.9.118",
38
- "@dbx-tools/shared-fs": "^0.9.118",
39
- "lru-cache": "11.5.3",
33
+ "@dbx-tools/auth": "^0.9.120",
34
+ "@dbx-tools/cache": "^0.9.120",
35
+ "@dbx-tools/core": "^0.9.120",
36
+ "@dbx-tools/lakebase": "^0.9.120",
37
+ "@dbx-tools/postgres": "^0.9.120",
38
+ "@dbx-tools/shared-core": "^0.9.120",
39
+ "@dbx-tools/shared-fs": "^0.9.120",
40
40
  "zod": "4.3.6"
41
41
  },
42
42
  "main": "./lib/index.js",
@@ -62,6 +62,10 @@
62
62
  "types": "./lib/src/bundle.d.ts",
63
63
  "default": "./lib/src/bundle.js"
64
64
  },
65
+ "./cache-storage": {
66
+ "types": "./lib/src/cache-storage.d.ts",
67
+ "default": "./lib/src/cache-storage.js"
68
+ },
65
69
  "./child-process": {
66
70
  "types": "./lib/src/child-process.d.ts",
67
71
  "default": "./lib/src/child-process.js"
@@ -110,6 +114,10 @@
110
114
  "types": "./lib/src/request-tags.d.ts",
111
115
  "default": "./lib/src/request-tags.js"
112
116
  },
117
+ "./tool-operations": {
118
+ "types": "./lib/src/tool-operations.d.ts",
119
+ "default": "./lib/src/tool-operations.js"
120
+ },
113
121
  "./tool-provider": {
114
122
  "types": "./lib/src/tool-provider.d.ts",
115
123
  "default": "./lib/src/tool-provider.js"
@@ -125,7 +133,7 @@
125
133
  "./package.json": "./package.json"
126
134
  }
127
135
  },
128
- "version": "0.9.118",
136
+ "version": "0.9.120",
129
137
  "types": "./lib/index.d.ts",
130
138
  "dbxToolsConfig": {
131
139
  "tags": [
@@ -150,6 +158,10 @@
150
158
  "types": "./lib/src/bundle.d.ts",
151
159
  "default": "./lib/src/bundle.js"
152
160
  },
161
+ "./cache-storage": {
162
+ "types": "./lib/src/cache-storage.d.ts",
163
+ "default": "./lib/src/cache-storage.js"
164
+ },
153
165
  "./child-process": {
154
166
  "types": "./lib/src/child-process.d.ts",
155
167
  "default": "./lib/src/child-process.js"
@@ -198,6 +210,10 @@
198
210
  "types": "./lib/src/request-tags.d.ts",
199
211
  "default": "./lib/src/request-tags.js"
200
212
  },
213
+ "./tool-operations": {
214
+ "types": "./lib/src/tool-operations.d.ts",
215
+ "default": "./lib/src/tool-operations.js"
216
+ },
201
217
  "./tool-provider": {
202
218
  "types": "./lib/src/tool-provider.d.ts",
203
219
  "default": "./lib/src/tool-provider.js"
package/src/appkit.ts CHANGED
@@ -39,8 +39,8 @@ import { profile as authProfile } from "@dbx-tools/auth";
39
39
  import { asyncUtils, environmentUtils, log } from "@dbx-tools/shared-core";
40
40
 
41
41
  import { resolveAutoConfigurePolicy } from "./_auto-configure.ts";
42
- import { createSoftPersistentStorage } from "./_cache-storage.ts";
43
42
  import { loadBrandContext } from "./brand-context.ts";
43
+ import { createAppKitCacheStorage } from "./cache-storage.ts";
44
44
  import {
45
45
  createInterceptorContext,
46
46
  type Interceptor,
@@ -48,6 +48,7 @@ import {
48
48
  lifecycleBridge,
49
49
  type ResolvedAppEnv,
50
50
  } from "./interceptor.ts";
51
+ import { installAsUserDevelopmentPatch } from "./identity.ts";
51
52
  import { applyLakebaseEnv, type LakebaseConnection } from "./lakebase-resolver.ts";
52
53
  import { provisionCacheSchema } from "./provision.ts";
53
54
 
@@ -244,20 +245,12 @@ function applyDefaultProfile(): string | undefined {
244
245
  return resolved.name;
245
246
  }
246
247
 
247
- /**
248
- * Whether `createApp` should inject soft-fail Lakebase cache storage.
249
- *
250
- * Skips when the caller disabled the cache, already supplied a storage
251
- * backend (including in-memory), or Lakebase is not in play.
252
- */
253
- function shouldInjectSoftPersistentCache(
254
- config: CreateAppConfig | undefined,
255
- lakebase: LakebaseConnection | undefined,
256
- ): boolean {
248
+ /** Whether `createApp` should inject the default memory-first cache storage. */
249
+ function shouldInjectCacheStorage(config: CreateAppConfig | undefined): boolean {
257
250
  const cache = config?.cache;
258
251
  if (cache?.enabled === false) return false;
259
252
  if (cache?.storage) return false;
260
- return Boolean(lakebase) || usesPlugin(config, LAKEBASE_PLUGIN);
253
+ return true;
261
254
  }
262
255
 
263
256
  function redactLakebaseConnection(resolved: LakebaseConnection): Record<string, unknown> {
@@ -306,6 +299,7 @@ function interceptorList(interceptor: CreateAppConfig["interceptor"]): Intercept
306
299
  export async function createApp<T extends AppKitPlugins>(
307
300
  config?: CreateAppConfig<T>,
308
301
  ): Promise<PluginMap<T>> {
302
+ installAsUserDevelopmentPatch();
309
303
  const plugins = pluginNames(config);
310
304
  const interceptors = interceptorList(config?.interceptor);
311
305
  logger.debug("createApp: start", {
@@ -328,15 +322,16 @@ export async function createApp<T extends AppKitPlugins>(
328
322
  delete appConfig.autoConfigure;
329
323
  delete appConfig.interceptor;
330
324
 
331
- if (shouldInjectSoftPersistentCache(config, lakebase)) {
332
- logger.debug("createApp: inject soft persistent cache storage");
333
- const storage = await createSoftPersistentStorage(config?.cache);
334
- if (storage) {
335
- appConfig.cache = { ...config?.cache, storage };
336
- logger.debug("createApp: soft persistent cache storage ready");
337
- } else {
338
- logger.debug("createApp: soft persistent cache storage unavailable");
339
- }
325
+ if (shouldInjectCacheStorage(config)) {
326
+ logger.debug("createApp: inject memory-first cache storage");
327
+ const storage = await createAppKitCacheStorage({
328
+ cache: config?.cache,
329
+ preferLakebase: Boolean(lakebase) || usesPlugin(config, LAKEBASE_PLUGIN),
330
+ });
331
+ appConfig.cache = { ...config?.cache, storage };
332
+ logger.debug("createApp: memory-first cache storage ready", {
333
+ persistent: storage.isPersistent(),
334
+ });
340
335
  }
341
336
 
342
337
  if (interceptors.length === 0) {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Soft-fail Lakebase cache storage for AppKit's persistent cache.
2
+ * Memory-first AppKit cache storage with soft-fail Lakebase persistence.
3
3
  *
4
4
  * AppKit's `PersistentStorage.initialize()` runs DDL migrations and throws on
5
5
  * any step failure. With `cache.strictPersistence: true` that throw makes
@@ -8,18 +8,25 @@
8
8
  * usable table keeps serving.
9
9
  *
10
10
  * `PersistentStorage` is not on AppKit's public export map. The deep load is
11
- * lazy and best-effort: if the installed AppKit layout changes, this returns
12
- * `undefined` and `createApp` leaves AppKit on its default cache path.
11
+ * lazy and best-effort: if the installed AppKit layout changes, Lakebase is
12
+ * skipped and the adapter keeps its project-scoped disk persistence.
13
13
  *
14
14
  * @module
15
15
  */
16
16
 
17
+ import { EventEmitter } from "node:events";
17
18
  import { createRequire } from "node:module";
18
19
  import { dirname, join } from "node:path";
19
20
  import { createLakebasePool, getWorkspaceClient, type CacheConfig } from "@databricks/appkit";
21
+ import {
22
+ closeCache,
23
+ createCache,
24
+ flushCache,
25
+ projectCache,
26
+ type CacheOptions,
27
+ } from "@dbx-tools/cache/cache";
20
28
  import { postgresConnectionOptions } from "@dbx-tools/postgres";
21
29
  import { errorUtils, hash, log } from "@dbx-tools/shared-core";
22
- import { LRUCache } from "lru-cache";
23
30
  import { handleOwnershipMigrationError } from "./migration.ts";
24
31
 
25
32
  const logger = log.logger("cache-storage");
@@ -32,9 +39,17 @@ interface CacheEntry<T = unknown> {
32
39
  value: T;
33
40
  }
34
41
 
35
- /** Bounds for the process-local cache layered over persistent AppKit storage. */
36
- export interface L1CacheStorageOptions {
37
- maxBytes?: number;
42
+ /** Options for AppKit's memory-first cache storage adapter. */
43
+ export interface AppKitCacheStorageOptions {
44
+ /** Native AppKit cache configuration. */
45
+ cache?: CacheConfig;
46
+ /** Whether to attempt AppKit's Lakebase PersistentStorage. Defaults to true. */
47
+ preferLakebase?: boolean;
48
+ /** Portable disk scope used when Lakebase is unavailable. */
49
+ diskScope?: string;
50
+ /** Write-behind coalescing delay in milliseconds. */
51
+ writeDelayMs?: number;
52
+ /** Maximum entries retained in the process-local memory primary. */
38
53
  maxEntries?: number;
39
54
  }
40
55
 
@@ -54,95 +69,126 @@ type PersistentStorageConstructor = new (
54
69
 
55
70
  let persistentStorageCtor: PersistentStorageConstructor | undefined | null = null;
56
71
 
57
- /** Process-local LRU that avoids a persistent cache query on every warm hit. */
58
- export class L1CacheStorage implements CacheStorage {
59
- private readonly entries: LRUCache<string, CacheEntry>;
72
+ class AppKitStorageLevel extends EventEmitter {
73
+ readonly opts = {};
74
+
75
+ constructor(private readonly storage: CacheStorage) {
76
+ super();
77
+ }
78
+
79
+ async get<T>(key: string): Promise<T | undefined> {
80
+ const entry = await this.storage.get<T>(key);
81
+ if (!entry) return undefined;
82
+ if (entry.expiry <= Date.now()) {
83
+ await this.storage.delete(key);
84
+ return undefined;
85
+ }
86
+ return entry.value;
87
+ }
88
+
89
+ async set(key: string, value: unknown, ttl?: number): Promise<void> {
90
+ await this.storage.set(key, {
91
+ value,
92
+ expiry: Date.now() + (ttl ?? 60_000),
93
+ });
94
+ }
95
+
96
+ async delete(key: string): Promise<boolean> {
97
+ await this.storage.delete(key);
98
+ return true;
99
+ }
100
+
101
+ clear(): Promise<void> {
102
+ return this.storage.clear();
103
+ }
104
+
105
+ has(key: string): Promise<boolean> {
106
+ return this.storage.has(key);
107
+ }
108
+
109
+ disconnect(): Promise<void> {
110
+ return this.storage.close();
111
+ }
112
+ }
113
+
114
+ /** Memory-first AppKit CacheStorage backed by @dbx-tools/cache. */
115
+ export class AppKitCacheStorage implements CacheStorage {
116
+ private readonly cache: ReturnType<typeof createCache>;
117
+ private readonly keys = new Set<string>();
118
+ private closed = false;
60
119
 
61
120
  constructor(
62
- private readonly storage: CacheStorage,
63
- options: L1CacheStorageOptions = {},
121
+ cache: ReturnType<typeof createCache>,
122
+ private readonly persistent: CacheStorage | undefined,
64
123
  ) {
65
- this.entries = new LRUCache({
66
- max: positiveBound(options.maxEntries, 1_000, "maxEntries"),
67
- maxSize: positiveBound(options.maxBytes, 64 * 1024 * 1024, "maxBytes"),
68
- sizeCalculation: (entry, key) => retainedSize(key, entry),
69
- });
124
+ this.cache = cache;
70
125
  }
71
126
 
72
127
  async get<T>(key: string): Promise<CacheEntry<T> | null> {
73
- const local = this.entries.get(key);
74
- if (local) return local as CacheEntry<T>;
75
- const persisted = await this.storage.get<T>(key);
76
- if (persisted && persisted.expiry > Date.now()) this.retain(key, persisted);
77
- return persisted;
128
+ const entry = await this.cache.get<CacheEntry<T>>(key);
129
+ if (!entry) return null;
130
+ this.keys.add(key);
131
+ return entry;
78
132
  }
79
133
 
80
134
  async set<T>(key: string, entry: CacheEntry<T>): Promise<void> {
81
- await this.storage.set(key, entry);
82
- this.retain(key, entry);
135
+ const ttl = entry.expiry - Date.now();
136
+ if (ttl <= 0) {
137
+ await this.delete(key);
138
+ return;
139
+ }
140
+ await this.cache.set(key, entry, { ttl });
141
+ this.keys.add(key);
83
142
  }
84
143
 
85
144
  async delete(key: string): Promise<void> {
86
- await this.storage.delete(key);
87
- this.entries.delete(key);
145
+ await this.cache.delete(key);
146
+ this.keys.delete(key);
88
147
  }
89
148
 
90
149
  async clear(): Promise<void> {
91
- await this.storage.clear();
92
- this.entries.clear();
150
+ await this.cache.clear();
151
+ this.keys.clear();
93
152
  }
94
153
 
95
154
  async has(key: string): Promise<boolean> {
96
- if (this.entries.has(key)) return true;
97
- return this.storage.has(key);
155
+ return this.cache.has(key);
98
156
  }
99
157
 
100
- size(): Promise<number> {
101
- return this.storage.size();
158
+ async size(): Promise<number> {
159
+ return this.persistent ? this.persistent.size() : this.keys.size;
102
160
  }
103
161
 
104
162
  isPersistent(): boolean {
105
- return this.storage.isPersistent();
163
+ return true;
106
164
  }
107
165
 
108
- healthCheck(): Promise<boolean> {
109
- return this.storage.healthCheck();
166
+ async healthCheck(): Promise<boolean> {
167
+ return this.persistent ? this.persistent.healthCheck() : true;
168
+ }
169
+
170
+ /** Drain tracked write-behind persistence without closing the storage. */
171
+ flush(): Promise<void> {
172
+ return flushCache(this.cache);
110
173
  }
111
174
 
112
175
  async close(): Promise<void> {
113
- this.entries.clear();
114
- await this.storage.close();
176
+ if (this.closed) return;
177
+ await closeCache(this.cache);
178
+ this.closed = true;
179
+ this.keys.clear();
115
180
  }
116
181
 
117
182
  /** Forward AppKit's runtime-only persistent cleanup hook when available. */
118
183
  async cleanupExpired(): Promise<number> {
119
184
  const cleanup = (
120
- this.storage as CacheStorage & {
121
- cleanupExpired?: () => Promise<number>;
122
- }
123
- ).cleanupExpired;
124
- return cleanup ? cleanup.call(this.storage) : 0;
125
- }
126
-
127
- private retain<T>(key: string, entry: CacheEntry<T>): void {
128
- const ttl = entry.expiry - Date.now();
129
- if (ttl > 0) this.entries.set(key, entry, { ttl });
130
- }
131
- }
132
-
133
- function positiveBound(value: number | undefined, fallback: number, name: string): number {
134
- const resolved = value ?? fallback;
135
- if (!Number.isFinite(resolved) || resolved <= 0) {
136
- throw new RangeError(`${name} must be a finite positive number`);
137
- }
138
- return Math.floor(resolved);
139
- }
140
-
141
- function retainedSize(key: string, entry: CacheEntry): number {
142
- try {
143
- return Buffer.byteLength(key) + Buffer.byteLength(JSON.stringify(entry));
144
- } catch {
145
- return Number.MAX_SAFE_INTEGER;
185
+ this.persistent as
186
+ | (CacheStorage & {
187
+ cleanupExpired?: () => Promise<number>;
188
+ })
189
+ | undefined
190
+ )?.cleanupExpired;
191
+ return cleanup && this.persistent ? cleanup.call(this.persistent) : 0;
146
192
  }
147
193
  }
148
194
 
@@ -171,7 +217,7 @@ export function loadPersistentStorage(): PersistentStorageConstructor | undefine
171
217
  return loaded;
172
218
  } catch (err) {
173
219
  logger.debug("soft persistent cache skipped (PersistentStorage unavailable)", {
174
- error: errorUtils.errorMessage(err),
220
+ error: errorUtils.toError(err),
175
221
  });
176
222
  persistentStorageCtor = undefined;
177
223
  return undefined;
@@ -257,9 +303,9 @@ export async function probeStorage(storage: PersistentStorageBase): Promise<void
257
303
  /**
258
304
  * Build a soft-fail Lakebase cache storage when a pool can be created and
259
305
  * AppKit's PersistentStorage can be loaded. Returns `undefined` otherwise so
260
- * AppKit can fall through to its normal cache path.
306
+ * the caller can use portable disk persistence.
261
307
  */
262
- export async function createSoftPersistentStorage(
308
+ async function createPersistentStorage(
263
309
  cache: CacheConfig | undefined,
264
310
  ): Promise<CacheStorage | undefined> {
265
311
  const PersistentStorage = loadPersistentStorage();
@@ -281,13 +327,10 @@ export async function createSoftPersistentStorage(
281
327
  }
282
328
  await storage.initialize();
283
329
  await probeStorage(storage);
284
- return new L1CacheStorage(storage, {
285
- maxBytes: cache?.maxBytes,
286
- maxEntries: cache?.maxSize,
287
- });
330
+ return storage;
288
331
  } catch (err) {
289
332
  logger.debug("soft persistent cache unavailable", {
290
- error: errorUtils.errorMessage(err),
333
+ error: errorUtils.toError(err),
291
334
  });
292
335
  if (pool) {
293
336
  await pool.end().catch(() => {});
@@ -295,3 +338,27 @@ export async function createSoftPersistentStorage(
295
338
  return undefined;
296
339
  }
297
340
  }
341
+
342
+ /**
343
+ * Create AppKit CacheStorage with bounded memory first and tracked persistence.
344
+ * Lakebase remains AppKit's persistent owner when available; otherwise the
345
+ * shared project-scoped portable disk level is used.
346
+ */
347
+ export async function createAppKitCacheStorage(
348
+ options: AppKitCacheStorageOptions = {},
349
+ ): Promise<AppKitCacheStorage> {
350
+ const nativeCache = options.cache;
351
+ const persistent =
352
+ options.preferLakebase === false ? undefined : await createPersistentStorage(nativeCache);
353
+ const common: CacheOptions = {
354
+ maxEntries: options.maxEntries ?? nativeCache?.maxSize,
355
+ writeDelayMs: options.writeDelayMs,
356
+ };
357
+ const cache = persistent
358
+ ? createCache({
359
+ ...common,
360
+ levels: [{ name: "appkit-persistent", store: new AppKitStorageLevel(persistent) }],
361
+ })
362
+ : projectCache(options.diskScope ?? "appkit", common);
363
+ return new AppKitCacheStorage(cache, persistent);
364
+ }