Allow passing compute fn to the consturctor
diff --git a/ui/src/base/async_memo.ts b/ui/src/base/async_memo.ts
index 592775c..176cbc5 100644
--- a/ui/src/base/async_memo.ts
+++ b/ui/src/base/async_memo.ts
@@ -91,6 +91,10 @@
 // Simple alias for a function that returns a Promise of type T.
 type AsyncFunc<T> = () => Promise<T>;
 
+export type AsyncMemoCompute<T> = (
+  signal: CancellationSignal,
+) => Promise<T | typeof TASK_CANCELLED>;
+
 /**
  * Runs async functions one at a time with cancellation support.
  *
@@ -145,10 +149,9 @@
 
 export interface AsyncMemoOptions<T, K extends JSONCompatible<K>> {
   readonly key: K;
-  // Function to call when the key changes to fetch the memo data.
-  readonly compute: (
-    signal: CancellationSignal,
-  ) => Promise<T | typeof TASK_CANCELLED>;
+  // Function to call when the key changes to fetch the memo data. Overrides
+  // the compute function passed to the constructor, if present.
+  readonly compute?: AsyncMemoCompute<T>;
   // If provided, compute() only runs when this is truthy.
   readonly enabled?: boolean;
   // Keep returning the previous memo while the new one loads as long as only
@@ -182,6 +185,8 @@
  * results before running a new compute and when the memo is disposed.
  */
 export class AsyncMemo<T> {
+  private readonly queue: AtomicTaskQueue;
+  private readonly defaultCompute?: AsyncMemoCompute<T>;
   private cache?: Cache<T>;
   private pendingKey?: object;
   private disposed = false;
@@ -189,9 +194,22 @@
   // Stores error keyed by keyStr - thrown on next use() with same key
   private error?: {keyStr: string; error: Error};
 
+  constructor();
+  constructor(compute: AsyncMemoCompute<T>);
+  constructor(queue: AtomicTaskQueue, compute?: AsyncMemoCompute<T>);
   constructor(
-    private readonly queue: AtomicTaskQueue = new AtomicTaskQueue(),
-  ) {}
+    queueOrCompute:
+      AtomicTaskQueue | AsyncMemoCompute<T> = new AtomicTaskQueue(),
+    compute?: AsyncMemoCompute<T>,
+  ) {
+    if (typeof queueOrCompute === 'function') {
+      this.queue = new AtomicTaskQueue();
+      this.defaultCompute = queueOrCompute;
+    } else {
+      this.queue = queueOrCompute;
+      this.defaultCompute = compute;
+    }
+  }
 
   /**
    * Call every render cycle to get the current memoized result.
@@ -204,7 +222,8 @@
     if (this.disposed) {
       throw new Error('AsyncMemo.use() called after dispose()');
     }
-    const {key, compute, enabled, retainOn = []} = options;
+    const {key, enabled, retainOn = []} = options;
+    const compute = options.compute ?? this.defaultCompute;
     const keyStr = stringifyJsonWithBigints(key);
 
     // If we have a stored error for this key, throw it
@@ -226,6 +245,12 @@
     const canRun = enabled === undefined || enabled;
 
     if (isKeyDifferentFromPending && isKeyDifferentFromCache && canRun) {
+      if (compute === undefined) {
+        throw new Error(
+          'AsyncMemo requires a compute function in its constructor or use()',
+        );
+      }
+
       // Cancel any in-flight task
       if (this.currentSignal) {
         this.currentSignal.cancelled = true;
diff --git a/ui/src/base/async_memo_unittest.ts b/ui/src/base/async_memo_unittest.ts
index 9308716..03b85f2 100644
--- a/ui/src/base/async_memo_unittest.ts
+++ b/ui/src/base/async_memo_unittest.ts
@@ -42,6 +42,41 @@
   expect(queryFn).toHaveBeenCalledTimes(1);
 });
 
+test('uses compute function passed to constructor', async () => {
+  const queryFn = vi.fn().mockResolvedValue(42);
+  const slot = new AsyncMemo<number>(queryFn);
+
+  slot.use({key: {id: 1}});
+  await flushPromises();
+
+  const result = slot.use({key: {id: 1}});
+  expect(result.data).toBe(42);
+  expect(result.isPending).toBe(false);
+  expect(queryFn).toHaveBeenCalledTimes(1);
+});
+
+test('use compute function overrides constructor function', async () => {
+  const defaultQueryFn = vi.fn().mockResolvedValue(1);
+  const overrideQueryFn = vi.fn().mockResolvedValue(2);
+  const slot = new AsyncMemo<number>(defaultQueryFn);
+
+  slot.use({key: {id: 1}, compute: overrideQueryFn});
+  await flushPromises();
+
+  const result = slot.use({key: {id: 1}});
+  expect(result.data).toBe(2);
+  expect(defaultQueryFn).not.toHaveBeenCalled();
+  expect(overrideQueryFn).toHaveBeenCalledTimes(1);
+});
+
+test('throws when no compute function is provided', () => {
+  const slot = new AsyncMemo<number>();
+
+  expect(() => slot.use({key: {id: 1}})).toThrow(
+    'AsyncMemo requires a compute function in its constructor or use()',
+  );
+});
+
 test('cached result is returned without re-query', async () => {
   const slot = new AsyncMemo<number>();