Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
272649e
refactor(appkit): give each app its own CacheManager
IamGalymzhan Sep 1, 2026
f97987a
refactor(appkit): bind Plugin.cache from its app, read-only
IamGalymzhan Sep 1, 2026
141b0e2
fix(appkit): close the app's cache on a failed boot
IamGalymzhan Sep 1, 2026
031d964
fix(appkit): close only the storage a CacheManager built
IamGalymzhan Sep 2, 2026
70bcc1f
fix(playground): make the telemetry example actually cache
IamGalymzhan Sep 2, 2026
b66a302
feat(appkit): give createTestPluginContext its own real cache
IamGalymzhan Sep 2, 2026
e4a3fcf
feat(appkit): retarget resetTestCache off the process-wide slot
IamGalymzhan Sep 2, 2026
d43cf79
docs(appkit): regenerate the Plugin API reference for the cache accessor
IamGalymzhan Sep 2, 2026
b9583ef
test(appkit): drop the cache-module mock from suites that never cached
IamGalymzhan Sep 2, 2026
108db3d
test(appkit): assert cache behaviour against the real cache, not a fake
IamGalymzhan Sep 2, 2026
f1e35e3
test(appkit): assert metric cache keys against production's generateKey
IamGalymzhan Sep 2, 2026
8855eef
test(appkit): build managers directly in the CacheManager suite
IamGalymzhan Sep 2, 2026
a8e32a7
test(appkit): drop the static cache scaffolding from the agents suites
IamGalymzhan Sep 2, 2026
ffdbfcf
test(appkit): bind the framework suites' cache through a context
IamGalymzhan Sep 2, 2026
f019900
test(appkit): run the analytics suites on the real cache
IamGalymzhan Sep 2, 2026
2aaec52
test(appkit): run the ai-search suite on the real cache
IamGalymzhan Sep 2, 2026
38482c4
test(appkit): run the files plugin suite on the real cache
IamGalymzhan Sep 2, 2026
a147625
test(appkit): drop the last cache-module mock
IamGalymzhan Sep 2, 2026
206740b
feat(appkit): remove the process-wide CacheManager
IamGalymzhan Sep 2, 2026
937c8a0
docs(appkit): correct the testing kit's stale singleton wording
IamGalymzhan Sep 2, 2026
e25fbaa
fix(appkit): end the Lakebase pool when a healthy cache fails to init
IamGalymzhan Sep 2, 2026
0136e0f
test(appkit): drop the inert @databricks-apps/cache mock
IamGalymzhan Sep 2, 2026
cd9d436
refactor(appkit): make Plugin.cache fail closed for direct readers
IamGalymzhan Sep 2, 2026
a3d0bc7
test(appkit): cover standalone runAgent with a real Plugin subclass
IamGalymzhan Sep 2, 2026
08ed041
refactor(appkit): make the kit's attach() synchronous and dedupe plug…
IamGalymzhan Sep 2, 2026
0d03d90
refactor(appkit): drop the full-config requirement from InMemoryStorage
IamGalymzhan Sep 2, 2026
36a4069
docs(appkit): correct comments that describe the deleted process-wide…
IamGalymzhan Sep 2, 2026
acd49ae
docs(appkit): state the accurate cache-isolation guarantee
IamGalymzhan Sep 2, 2026
2288545
fix(appkit): log when a healthy Lakebase cache fails to initialize
IamGalymzhan Sep 2, 2026
be303a0
fix(appkit): register a plugin name once in the kit's attach()
IamGalymzhan Sep 2, 2026
7f08622
docs(appkit): regenerate the Plugin API reference for the cache accessor
IamGalymzhan Sep 2, 2026
5891326
docs(appkit): cut change-narration and over-long comments in the cach…
IamGalymzhan Sep 2, 2026
aaaf378
refactor(appkit): fold the kit cache registry into fixtures.ts
IamGalymzhan Sep 2, 2026
a7079cf
docs(appkit): correct attach()'s registration comment to say first-wins
IamGalymzhan Sep 2, 2026
dfd2289
refactor(appkit): make PluginContext.cache required
IamGalymzhan Sep 2, 2026
c528330
Merge origin/main into refactor/cache-manager-per-app
IamGalymzhan Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/dev-playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"install": "cd client && npm install && cd ..",
"preview": "vite preview",
"check": "tsc",
"typecheck": "tsc --noEmit",
"clean": "rm -rf build && cd client && rm -rf dist",
"clean:full": "rm -rf build node_modules && cd client && rm -rf dist node_modules",
"test:integration": "playwright test",
Expand Down
11 changes: 7 additions & 4 deletions apps/dev-playground/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,10 +375,13 @@ createApp({
const pngs = new Map<string, (typeof entries)[number]>();
const metas = new Map<string, (typeof entries)[number]>();
for (const e of entries) {
if (e.path.endsWith(".png")) {
pngs.set(e.path.replace(/\.png$/, ""), e);
} else if (e.path.endsWith(".json")) {
metas.set(e.path.replace(/\.json$/, ""), e);
// `path` is optional on the SDK's entry type.
const path = e.path;
if (!path) continue;
if (path.endsWith(".png")) {
pngs.set(path.replace(/\.png$/, ""), e);
} else if (path.endsWith(".json")) {
metas.set(path.replace(/\.json$/, ""), e);
}
}
const views = await Promise.all(
Expand Down
2 changes: 0 additions & 2 deletions apps/dev-playground/server/telemetry-example-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import {
type BasePluginConfig,
CacheManager,
type Counter,
type Histogram,
Plugin,
Expand Down Expand Up @@ -32,7 +31,6 @@ class TelemetryExamples extends Plugin {

constructor(config: BasePluginConfig) {
super(config);
this.cache = new CacheManager({ enabled: true, ttl: 60 }, this.telemetry);

const meter = this.telemetry.getMeter({ name: "custom-telemetry-example" });
this.requestCounter = meter.createCounter("app.requests.total", {
Expand Down
6 changes: 4 additions & 2 deletions apps/dev-playground/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"declaration": false,
"declarationMap": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
"emitDecoratorMetadata": true,
"customConditions": ["development"]
},
"exclude": ["node_modules", "build"]
"exclude": ["node_modules", "build", "client"],
"include": ["server/**/*.ts", "*.ts"]
}
44 changes: 32 additions & 12 deletions docs/docs/api/appkit/Class.Plugin.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions docs/docs/api/appkit/Interface.AgentsPluginConfig.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions docs/docs/api/appkit/Interface.BasePluginConfig.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions docs/docs/api/appkit/Interface.IAiSearchConfig.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions docs/docs/api/appkit/Interface.IJobsConfig.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 66 additions & 0 deletions docs/docs/plugins/caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,69 @@ const value = await this.cache.getOrExecute(
{ ttl: 300 },
);
```

### One cache per app

Each app owns exactly one cache. `createApp` builds it from your `cache` config
and hands the same manager to every plugin it registers, so `this.cache` is the
app's cache — not a process-wide one. Two apps in the same process hold two
independent managers, and each honours its own `cache` config.

`this.cache` is read-only: a plugin cannot substitute its own manager. To vary
caching per plugin, set a plugin-level `cache` config instead:

```ts
analytics({ cache: { enabled: true, ttl: 600 } });
```

A plugin receives its cache when the app registers it. Construction alone does
not bind one, so `this.cache` is not available in a plugin's constructor —
read it from `setup()` or from a request handler, both of which run after
registration.

## Upgrading to 0.71.0

The cache became per-app in 0.71.0. Most apps need no changes: if your plugins
reach the cache through `this.cache` and you build apps with `createApp`, this
release is a no-op for you.

**`CacheManager.getInstance()` and `CacheManager.getInstanceSync()` are
removed.** There is no process-wide cache to fetch. Inside a plugin, use
`this.cache`, which the app binds for you:

```ts
// Before
const cache = CacheManager.getInstanceSync();
await cache.getOrExecute(["k"], work, userKey);

// After
await this.cache.getOrExecute(["k"], work, userKey);
```

An app's cache is handed only to the plugins it registers — there is no
process-wide accessor to fetch it from. Move the cached work into a plugin.

**A plugin constructed without an app has no cache.** Previously such a plugin
picked up whichever manager happened to exist in the process. Now a cached
execution on an unregistered plugin throws `InitializationError`
(`CacheManager not initialized`) naming the plugin. Register it through
`createApp`, or in tests attach it to a test context:

```ts
import { createTestPluginContext } from "@databricks/appkit/testing";

const mock = createTestPluginContext();
await mock.attach(new MyPlugin({}));
// mock.cache is the very cache the plugin now resolves — spy or read it.
```

The most common way to hit this is reading `this.cache` in a plugin's
constructor — which always runs before any attach — or on a plugin you built
by hand and never registered. Under `createApp`, `setup()` and request
handlers run after the cache is bound, so a registered plugin is safe (this is
why [above](#one-cache-per-app) says to read the cache from `setup()`).

**`this.cache` is read-only.** A plugin that assigned its own manager
(`this.cache = new CacheManager(...)`) no longer compiles. Use a plugin-level
`cache: { enabled, ttl }` config instead — see [One cache per
app](#one-cache-per-app).
23 changes: 18 additions & 5 deletions docs/docs/plugins/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const mock = createTestPluginContext({

### Attaching to a plugin

`attach()` wires the context to a plugin the production way: it seeds an in-memory cache (if AppKit hasn't already initialized one), then calls the plugin's `attachContext`, which rebuilds telemetry and flips `isReady` to `true`. Await it before exercising any handler that reads `this.context`, `this.cache`, or gates on `isReady`:
`attach()` wires the context to a plugin the production way: it calls the plugin's `attachContext`, which binds this context's cache, rebuilds telemetry, and flips `isReady` to `true`. Await it before exercising any handler that reads `this.context`, `this.cache`, or gates on `isReady`:

```ts
const plugin = new MyAgentPlugin({});
Expand All @@ -58,17 +58,30 @@ await mock.attach(plugin);

Instantiate the plugin **class** directly (`new MyAgentPlugin(...)`). The `analytics()` / `agents()` factories you pass to `createApp` return a descriptor for the app to construct — for a unit test you want the instance.

The cache `attach()` seeds is a process-wide singleton: `CacheManager` is initialized once per test process and reused. Vitest isolates test *files* in separate workers, so caches never leak across files, but tests **within one file** share it. If a test populates the cache and a later test in the same file must not see it, clear it between tests with `resetTestCache()`:
Each `createTestPluginContext()` carries its **own** real in-memory cache, exposed as `mock.cache` — the very object the attached plugin resolves as `this.cache`. Two contexts in one file cannot see each other's entries, and nothing is shared with other test files.

That makes it the seam for asserting real caching behaviour, against production's own `getOrExecute` and `generateKey` rather than a re-implemented fake:

```ts
const mock = createTestPluginContext();
const plugin = await mock.attach(new MyPlugin({}));

const getOrExecute = vi.spyOn(mock.cache, "getOrExecute");
await plugin.handleRequest(req, res);
expect(getOrExecute).toHaveBeenCalledOnce();
```

Entries persist across tests in a file, since the context is built where you build it. Clear between tests — or mid-test, to force a miss before asserting the next call is a hit — with `resetTestCache()`:

```ts
import { resetTestCache } from "@databricks/appkit/testing";

beforeEach(async () => {
await resetTestCache(); // no-op if the cache isn't initialized yet
await resetTestCache(); // every cache this file's contexts built
});
```

It also helps *within* a single test — clear the cache to force a miss, then assert the following call is a hit.
Pass a specific context (`resetTestCache(mock)`) or manager (`resetTestCache(mock.cache)`) to clear just that one.

### Inspecting what happened

Expand Down Expand Up @@ -159,7 +172,7 @@ The kit re-exports the request/response/context fixtures AppKit uses internally:
```
- `createSuccessfulSQLResponse(rows, columns)` / `createFailedSQLResponse(message)` — build SQL Warehouse statement responses.
- `setupDatabricksEnv(overrides?)` — set `DATABRICKS_HOST` / `DATABRICKS_WAREHOUSE_ID` to test values.
- `resetTestCache()` — clear the shared cache singleton between (or within) tests; no-ops if the cache isn't initialized yet.
- `resetTestCache(target?)` — clear the caches this file's test contexts built, between or within tests. Pass a context or a manager to clear only that one; no-ops when there is nothing to clear.

## Full example

Expand Down
Loading
Loading