Skip to content

[FOEPD-3391] Implement ONNX model weight download, IndexedDB caching#7199

Open
yrahal wants to merge 11 commits intodevelopfrom
youcef/sam2_2
Open

[FOEPD-3391] Implement ONNX model weight download, IndexedDB caching#7199
yrahal wants to merge 11 commits intodevelopfrom
youcef/sam2_2

Conversation

@yrahal
Copy link
Contributor

@yrahal yrahal commented Mar 17, 2026

What changes are proposed in this pull request?

Add modelCache that downloads encoder and decoder weights with streaming progress reporting and retry logic, then caches the weights in IndexedDB. On subsequent visits, weights are directly served from cache.

Worker now uses loadModelWeights instead of bare fetch, and forwards progress/warning notifications to the main thread via typed postMessage helpers.

How is this patch tested? If it is not, please explain why.

  • Unit tests
  • In-app [local UI changes not committed]

Release Notes

Is this a user-facing change that should be mentioned in the release notes?

  • No. You can skip the rest of this section.
  • Yes. Give a description of this change to be included in the release
    notes for FiftyOne users.

(Details in 1-2 sentences. You can just refer to another PR with a description
if this PR is part of a larger change.)

What areas of FiftyOne does this PR affect?

  • App: FiftyOne application changes
  • Build: Build and test infrastructure changes
  • Core: Core fiftyone Python library changes
  • Documentation: FiftyOne documentation changes
  • Other

Summary by CodeRabbit

  • New Features

    • Initialization now accepts optional progress and warning callbacks to report download progress and surface warnings.
    • Workers emit progress and warning notifications during model loading.
    • Client-side IndexedDB caching for model weights with retry/backoff, timeout, streaming progress, and fallback-on-cache-failure; cache hits report immediate progress.
  • Tests

    • Added tests covering cache behavior, streaming and cache-hit progress reporting, retry/backoff and timeout retries, warning paths, and error scenarios.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 17, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ba224cc9-b64c-4e40-81c4-7e33be86ebf3

📥 Commits

Reviewing files that changed from the base of the PR and between 43ae192 and 5a3d5a2.

📒 Files selected for processing (2)
  • app/packages/annotation/src/providers/modelCache.test.ts
  • app/packages/annotation/src/providers/modelCache.ts

Walkthrough

Adds an IndexedDB-backed model weights cache and a resilient fetch helper with retries, timeout, and streaming progress reporting (modelCache.ts exporting loadModelWeights and MAX_RETRIES). Introduces typed download progress and warning callback types and worker-to-main notification helpers. The worker now uses loadModelWeights to fetch encoder/decoder weights, emits progress and warning notifications via standardized messaging helpers, and builds sessions from cached buffers when available. BrowserAnnotationProvider.initialize accepts onProgress and onWarning, forwards worker notifications to those callbacks, and clears them on dispose. New Vitest tests exercise cache, streaming progress, retries, and IndexedDB failure modes.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant BAP as BrowserAnnotationProvider
    participant Worker
    participant Cache as modelCache
    participant IDB as IndexedDB
    participant Net as Network

    Client->>BAP: initialize(onProgress,onWarning)
    BAP->>BAP: store callbacks
    BAP->>Worker: postMessage(loadModel request)

    Worker->>Cache: loadModelWeights(url, onProgress, onWarning)
    Cache->>IDB: openDB()
    IDB-->>Cache: db handle or error

    alt DB open & cache hit
        Cache->>IDB: idbGet(key)
        IDB-->>Cache: ArrayBuffer
        Cache->>Worker: return ArrayBuffer (invoke onProgress(size,size))
    else DB open failure or cache miss
        Cache->>Worker: postWarning(message) if DB open/read error
        Cache->>Net: fetchWithProgress(url) (retries, timeout)
        Net-->>Cache: stream chunks / final response
        Cache->>Worker: invoke onProgress(loaded,total) per chunk
        Cache->>IDB: idbPut(key, buffer) (warn on failure)
        Cache-->>Worker: return ArrayBuffer
    end

    Worker->>BAP: postNotification(progress/warning)
    BAP->>Client: call onProgress/onWarning callbacks
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: implementing ONNX model weight download with IndexedDB caching.
Description check ✅ Passed The description covers proposed changes, testing approach, and correctly marks as non-user-facing. All required sections are addressed with appropriate detail.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch youcef/sam2_2

Comment @coderabbitai help to get the list of available commands and usage tips.

@yrahal yrahal changed the title [FOEPD-3391] Implement ONNX model weight download, IndexedDB caching3391 [FOEPD-3391] Implement ONNX model weight download, IndexedDB caching Mar 17, 2026
Base automatically changed from youcef/sam2 to develop March 18, 2026 18:35
yrahal added 2 commits March 18, 2026 18:46
Add modelCache that downloads encoder and decoder weights with
streaming progress reporting and retry logic, then caches the weights
in IndexedDB. On subsequent visits, weights are directly served from cache.

Worker now uses loadModelWeights instead of bare fetch, and forwards
progress/warning notifications to the main thread via typed postMessage helpers.
@yrahal yrahal marked this pull request as ready for review March 18, 2026 18:55
@yrahal yrahal requested a review from a team as a code owner March 18, 2026 18:55
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/packages/annotation/src/providers/BrowserAnnotationProvider.ts`:
- Around line 26-28: The implementation of BrowserAnnotationProvider.initialize
accepts optional onProgress and onWarning callbacks but the AnnotationProvider
interface's initialize(): Promise<void> has no parameters, causing a type-safety
gap; either (1) update the AnnotationProvider interface in types.ts to declare
initialize(onProgress?: DownloadProgressCallback, onWarning?: WarningCallback):
Promise<void> (import/declare the DownloadProgressCallback and WarningCallback
types there) so the public contract matches
BrowserAnnotationProvider.initialize, or (2) remove the extra parameters from
BrowserAnnotationProvider.initialize (and any callers that rely on them) so its
signature conforms to the existing interface; target the initialize method in
BrowserAnnotationProvider and the AnnotationProvider interface in types.ts when
making the change.

In `@app/packages/annotation/src/providers/modelCache.ts`:
- Around line 70-75: The code treats a missing Content-Length as total = 0 which
causes progress callbacks to receive (loaded, 0) and can lead to
division-by-zero in consumers; change the logic so a missing/unknown
content-length is represented as undefined (or null) and only invoke
onProgress-based percentage calculations when total is a positive number.
Concretely: replace Number(response.headers.get("content-length")) || 0 with
logic that sets total = Number(...) only if the header exists and >0 (otherwise
total = undefined), keep the existing fast-path return when !onProgress ||
!response.body, and in the streaming loop (the code using response.body and
onProgress) guard calls that compute percentages so they run only if total is a
positive number (otherwise either call onProgress with loaded and undefined or
skip calling it entirely).
- Around line 116-147: The loadModelWeights function can leak an open
IDBDatabase handle if openDB() succeeds but fetchWithProgress(url, onProgress)
throws; update loadModelWeights so any opened db is always closed: after setting
db = await openDB(), ensure subsequent operations (cache read,
fetchWithProgress, idbPut) are wrapped so db.close() is invoked in a finally
block (or otherwise guaranteed) when db is non-null; specifically adjust the
control flow around openDB(), idbGet, fetchWithProgress, and idbPut to call
db.close() in a finally section so no handles remain open on errors.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a4c9e5c7-1727-4d03-a087-fddbeaca7ce5

📥 Commits

Reviewing files that changed from the base of the PR and between 3e4c6ec and 69858d0.

📒 Files selected for processing (5)
  • app/packages/annotation/src/providers/BrowserAnnotationProvider.ts
  • app/packages/annotation/src/providers/modelCache.test.ts
  • app/packages/annotation/src/providers/modelCache.ts
  • app/packages/annotation/src/providers/types.ts
  • app/packages/annotation/src/providers/worker.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/packages/annotation/src/providers/modelCache.ts`:
- Line 104: The code currently does `throw lastError;` which can be null when
MAX_RETRIES <= 0; update the retry/failure handling in the modelCache.ts routine
that uses `lastError` and `MAX_RETRIES` so it always throws a proper Error
object—e.g., if `lastError` is falsy create and throw a new Error with a clear
message (or wrap the original `lastError`), ensuring the throwing site (the
variable `lastError` and the retry loop around `MAX_RETRIES`) never forwards a
null/undefined value to `throw`.
- Around line 123-132: The function leaks the IndexedDB handle on cache hits
because the early return after idbGet(url) bypasses the later finally that
closes the DB; refactor the function so the entire body that uses the local db
(the openDB() call, idbGet(), onProgress/onWarning usage and the subsequent
download logic) is wrapped in a single try...finally and ensure the finally
always closes the db (the same db variable returned from openDB) so cached hits
also trigger closure; locate openDB and idbGet usage in modelCache.ts and
move/merge the existing cleanup logic into that single finally block to
guarantee db closure on every exit.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b5a90d96-fc3b-40c4-807b-543f59f0b602

📥 Commits

Reviewing files that changed from the base of the PR and between 69858d0 and 5b89701.

📒 Files selected for processing (1)
  • app/packages/annotation/src/providers/modelCache.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
app/packages/annotation/src/providers/modelCache.ts (1)

124-133: ⚠️ Potential issue | 🔴 Critical

DB handle leak on cache hit remains unaddressed.

When cached is truthy, the function returns at line 129, bypassing the finally block at lines 147-148 (which is scoped to the second try block). Every cache hit leaks an IDBDatabase handle.

Wrap the entire function body in a single try-finally to ensure db?.close() runs on all exit paths.

🐛 Proposed fix: single try-finally wrapping entire body
 export async function loadModelWeights(
   url: string,
   onProgress?: (loaded: number, total: number) => void,
   onWarning?: (message: string) => void
 ): Promise<ArrayBuffer> {
   let db: IDBDatabase | null = null;
 
-  try {
-    db = await openDB();
-    const cached = await idbGet(db, url);
-    if (cached) {
-      onProgress?.(cached.byteLength, cached.byteLength);
-      return cached;
-    }
-  } catch (err) {
-    onWarning?.(`IndexedDB cache read failed, downloading instead: ${err}`);
-  }
-
   try {
-    const buffer = await fetchWithProgress(url, onProgress);
+    try {
+      db = await openDB();
+      const cached = await idbGet(db, url);
+      if (cached) {
+        onProgress?.(cached.byteLength, cached.byteLength);
+        return cached;
+      }
+    } catch (err) {
+      onWarning?.(`IndexedDB cache read failed, downloading instead: ${err}`);
+    }
 
+    const buffer = await fetchWithProgress(url, onProgress);
     if (db) {
       try {
         await idbPut(db, url, buffer);
       } catch (err) {
         onWarning?.(`IndexedDB cache write failed: ${err}`);
       }
     }
-
     return buffer;
   } finally {
     db?.close();
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/packages/annotation/src/providers/modelCache.ts` around lines 124 - 133,
The function leaks the IDBDatabase handle when a cache hit returns early because
the current finally that calls db?.close() only wraps the later try block;
refactor by wrapping the entire function body (including the initial
openDB()/idbGet/cache-return block that references db, idbGet, onProgress,
onWarning) in a single try...finally so that db?.close() is executed on all exit
paths; ensure you keep the existing logic and error handling (openDB(),
idbGet(), onProgress/onWarning) but move the finalizer that calls db?.close()
into the single finally that encloses those operations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/packages/annotation/src/providers/modelCache.test.ts`:
- Around line 5-41: Wrap the mock DB's no-op close in a spy and expose it from
createMockIDB so tests can assert the handle was closed: replace mockDB.close
with a function that calls a spy (e.g., closeSpy) and return closeSpy on the
object returned by createMockIDB; keep mockIndexedDB.open returning the mockDB
as before. Then add the test that stubs global indexedDB with idb.mockIndexedDB,
seeds idb.store with TEST_BUFFER at TEST_URL, calls loadModelWeights(TEST_URL),
and asserts idb.closeSpy was called. Ensure you reference createMockIDB,
mockIndexedDB, mockDB.close (now hooked to closeSpy), and loadModelWeights in
the change.

---

Duplicate comments:
In `@app/packages/annotation/src/providers/modelCache.ts`:
- Around line 124-133: The function leaks the IDBDatabase handle when a cache
hit returns early because the current finally that calls db?.close() only wraps
the later try block; refactor by wrapping the entire function body (including
the initial openDB()/idbGet/cache-return block that references db, idbGet,
onProgress, onWarning) in a single try...finally so that db?.close() is executed
on all exit paths; ensure you keep the existing logic and error handling
(openDB(), idbGet(), onProgress/onWarning) but move the finalizer that calls
db?.close() into the single finally that encloses those operations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 41c22335-be6c-4aa0-b872-3816cc7759b8

📥 Commits

Reviewing files that changed from the base of the PR and between 5b89701 and 138ca01.

📒 Files selected for processing (2)
  • app/packages/annotation/src/providers/modelCache.test.ts
  • app/packages/annotation/src/providers/modelCache.ts

@yrahal yrahal requested a review from tom-vx51 March 19, 2026 16:00
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
app/packages/annotation/src/providers/modelCache.ts (1)

105-105: ⚠️ Potential issue | 🟡 Minor

Defensively avoid throwing a nullable value.

throw lastError can throw null if retry count is ever configured to 0. Use a fallback Error.

🛡️ Suggested fix
-  throw lastError;
+  throw lastError ?? new Error(`Failed to fetch ${url} after ${MAX_RETRIES} attempts`);
#!/bin/bash
# Verify retry count and throw site.
rg -n -C3 'MAX_RETRIES|throw lastError' app/packages/annotation/src/providers/modelCache.ts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/packages/annotation/src/providers/modelCache.ts` at line 105, The throw
site uses `throw lastError`, which can be null when retries are configured to 0;
change the throw to use a non-null fallback (e.g., `throw lastError ?? new
Error("Model cache operation failed")`) so a valid Error is always thrown;
update the code that manages `lastError` and the `MAX_RETRIES`/retry loop in
modelCache.ts (referencing the `lastError` variable and the retry logic around
`MAX_RETRIES`) to ensure the thrown value is never nullable and preserves the
original error when present.
app/packages/annotation/src/providers/modelCache.test.ts (1)

17-40: 🧹 Nitpick | 🔵 Trivial

Assert DB handle cleanup explicitly in tests.

mockDB.close is a no-op, so cleanup regressions can pass unnoticed. Expose a closeSpy and assert it on cache-hit path.

🧪 Suggested enhancement
 function createMockIDB() {
   const store = new Map<string, ArrayBuffer>();
+  const closeSpy = vi.fn();
@@
   const mockDB = {
@@
-    close: () => {},
+    close: closeSpy,
   };
@@
-  return { store, mockIndexedDB };
+  return { store, mockIndexedDB, closeSpy };
 }
   it("Returns cached buffer without fetching on cache hit", async () => {
-    store.set(TEST_URL, TEST_BUFFER);
+    const idb = createMockIDB();
+    vi.stubGlobal("indexedDB", idb.mockIndexedDB);
+    idb.store.set(TEST_URL, TEST_BUFFER);
 
     const result = await loadModelWeights(TEST_URL);
 
     expect(result.byteLength).toBe(64);
     expect(global.fetch).not.toHaveBeenCalled();
+    expect(idb.closeSpy).toHaveBeenCalled();
   });

Also applies to: 88-95

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/packages/annotation/src/providers/modelCache.test.ts` around lines 17 -
40, The mocked DB's close method is a no-op so tests don't detect cleanup
regressions; replace that no-op with a spy (e.g., closeSpy) on the mockDB (the
object returned by the open() request in the mockIndexedDB) and return or expose
the spy alongside store/mockIndexedDB from the helper used in
modelCache.test.ts; then update the cache-hit test to explicitly assert closeSpy
was called (on the cache-hit path) to ensure DB handles are closed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/packages/annotation/src/providers/modelCache.ts`:
- Around line 50-57: The retry loop in modelCache.ts (the for loop using
attempt, MAX_RETRIES and RETRY_BACKOFF_MS) calls fetch(url) with no per-attempt
timeout which can hang indefinitely; modify the attempt body to create an
AbortController per attempt, pass its signal to fetch (fetch(url, { signal })),
start a timer that calls controller.abort() after a configured
PER_ATTEMPT_TIMEOUT_MS, await the fetch, and always clear the timeout (and
handle AbortError in the existing catch) so each attempt is bounded and the
retry loop can continue or fail cleanly.

---

Duplicate comments:
In `@app/packages/annotation/src/providers/modelCache.test.ts`:
- Around line 17-40: The mocked DB's close method is a no-op so tests don't
detect cleanup regressions; replace that no-op with a spy (e.g., closeSpy) on
the mockDB (the object returned by the open() request in the mockIndexedDB) and
return or expose the spy alongside store/mockIndexedDB from the helper used in
modelCache.test.ts; then update the cache-hit test to explicitly assert closeSpy
was called (on the cache-hit path) to ensure DB handles are closed.

In `@app/packages/annotation/src/providers/modelCache.ts`:
- Line 105: The throw site uses `throw lastError`, which can be null when
retries are configured to 0; change the throw to use a non-null fallback (e.g.,
`throw lastError ?? new Error("Model cache operation failed")`) so a valid Error
is always thrown; update the code that manages `lastError` and the
`MAX_RETRIES`/retry loop in modelCache.ts (referencing the `lastError` variable
and the retry logic around `MAX_RETRIES`) to ensure the thrown value is never
nullable and preserves the original error when present.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 74d9ad79-6de3-4e65-967e-df8e7c93755d

📥 Commits

Reviewing files that changed from the base of the PR and between 138ca01 and a7015bc.

📒 Files selected for processing (2)
  • app/packages/annotation/src/providers/modelCache.test.ts
  • app/packages/annotation/src/providers/modelCache.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/packages/annotation/src/providers/modelCache.test.ts`:
- Around line 189-198: Add a unit test that simulates a failing IndexedDB read
path so loadModelWeights falls back to fetch and emits the proper warning: stub
global indexedDB to return a successful open but supply a
transaction/objectStore.get that triggers onerror (with an error message like
"Read failed"), call loadModelWeights(TEST_URL, undefined, onWarning) and assert
the returned buffer byteLength is 64 and that onWarning was called with a
message containing "IndexedDB cache read failed"; reference the loadModelWeights
function and the idbGet/read behavior in your mock (reuse or adapt createMockIDB
and the open/transaction/objectStore.get mocks) and ensure closeSpy is not
called.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 30a4ee8f-cd6d-4041-b5c1-371ff228cdf1

📥 Commits

Reviewing files that changed from the base of the PR and between a7015bc and a38d1e1.

📒 Files selected for processing (2)
  • app/packages/annotation/src/providers/modelCache.test.ts
  • app/packages/annotation/src/providers/modelCache.ts

@yrahal yrahal requested a review from lanzhenw March 20, 2026 14:56
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/packages/annotation/src/providers/modelCache.ts`:
- Around line 60-65: The current error handling treats all 4xx responses as
non-retryable; update the conditional around response.status in the retry loop
(the block that sets lastError using response.status/response.statusText and
references response and url) so that 408 (Request Timeout) and 429 (Too Many
Requests) are treated as transient and retried: only break for 4xx statuses that
are not 408 or 429 (e.g., change the condition to break if response.status >=
400 && response.status < 500 && response.status !== 408 && response.status !==
429), otherwise continue to allow retries for 408/429. Ensure lastError is still
set before continuing so the final throw preserves the last failure.
- Around line 75-96: The current streaming code buffers every chunk into
chunks[] then allocates a second full Uint8Array (result) causing ~2x memory
use; change the logic to preallocate a single Uint8Array of size total (use the
existing total variable) and write each incoming chunk directly into that buffer
as you read from response.body.getReader(), updating an offset and calling
onProgress(loaded, total) as before, then return the preallocated buffer.buffer;
remove the temporary chunks array and the second copy to eliminate double
buffering (references: response.body.getReader(), reader, chunks, loaded,
result, offset, onProgress).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7ff9ab1d-7876-40ec-a5a6-c406312f5717

📥 Commits

Reviewing files that changed from the base of the PR and between 7094406 and 494a9d0.

📒 Files selected for processing (1)
  • app/packages/annotation/src/providers/modelCache.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
app/packages/annotation/src/providers/modelCache.ts (1)

85-87: ⚠️ Potential issue | 🟡 Minor

Potential RangeError if server sends more data than Content-Length.

If the server's actual response body exceeds the declared content-length, result.set(value, loaded) will throw when writing past the pre-allocated buffer boundary. This is an edge case (misbehaving server), but defensive handling prevents cryptic errors.

🛡️ Proposed defensive fix
       for (;;) {
         const { done, value } = await reader.read();
         if (done)
           break;

+        if (loaded + value.byteLength > result.byteLength) {
+          // Server sent more data than Content-Length claimed; expand buffer
+          const expanded = new Uint8Array(loaded + value.byteLength);
+          expanded.set(result.subarray(0, loaded));
+          result = expanded;
+        }
         result.set(value, loaded);
         loaded += value.byteLength;
         onProgress(loaded, total);
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/packages/annotation/src/providers/modelCache.ts` around lines 85 - 87,
The code using result.set(value, loaded) can throw a RangeError if the server
sends more bytes than the declared total; update the read loop in modelCache.ts
to defensively check if loaded + value.byteLength > total before calling
result.set, and handle that case by either writing only the slice that fits
(e.g., value.subarray(0, total - loaded)) and then breaking/finishing, or by
logging/throwing a clear error; ensure you still call onProgress with the
clamped final loaded value and avoid writing past the pre-allocated buffer.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/packages/annotation/src/providers/modelCache.test.ts`:
- Around line 185-210: Add a test that verifies loadModelWeights aborts a
stalled fetch via AbortController and retries: mock global.fetch so the first
call returns a never-resolving promise and the second call resolves to
mockFetchResponse(TEST_BUFFER), use vi.useFakeTimers(), call
loadModelWeights(TEST_URL) to get the promise, advance timers past
FETCH_TIMEOUT_MS (e.g. await vi.advanceTimersByTimeAsync(FETCH_TIMEOUT_MS + 1))
to trigger the AbortController timeout, then advance timers to process retry
backoff, await the promise and assert the final buffer, that global.fetch was
called twice, and any relevant cleanup spies (e.g. closeSpy) were invoked;
reference loadModelWeights and FETCH_TIMEOUT_MS to locate the implementation.

In `@app/packages/annotation/src/providers/modelCache.ts`:
- Line 90: The code currently returns result.buffer which may contain trailing
uninitialized bytes if the stream closed early; in the function that reads
weights (referencing result.buffer, loaded and total), validate that loaded ===
total and if not either throw an error or return a trimmed buffer (e.g., slice
result.buffer to loaded) to avoid silent corruption of ONNX weights; update the
return path to use the trimmed buffer when loaded < total and ensure any error
path logs/throws a clear error indicating the truncation.

---

Duplicate comments:
In `@app/packages/annotation/src/providers/modelCache.ts`:
- Around line 85-87: The code using result.set(value, loaded) can throw a
RangeError if the server sends more bytes than the declared total; update the
read loop in modelCache.ts to defensively check if loaded + value.byteLength >
total before calling result.set, and handle that case by either writing only the
slice that fits (e.g., value.subarray(0, total - loaded)) and then
breaking/finishing, or by logging/throwing a clear error; ensure you still call
onProgress with the clamped final loaded value and avoid writing past the
pre-allocated buffer.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ed6eaf91-6d7e-412a-9d5f-9bc976d92771

📥 Commits

Reviewing files that changed from the base of the PR and between 494a9d0 and 309e62a.

📒 Files selected for processing (2)
  • app/packages/annotation/src/providers/modelCache.test.ts
  • app/packages/annotation/src/providers/modelCache.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
app/packages/annotation/src/providers/modelCache.ts (1)

90-90: ⚠️ Potential issue | 🟠 Major

Throw on truncated streams instead of returning partial weights.

Returning result.slice(0, loaded).buffer turns a short read into a successful fetch, and loadModelWeights() will then persist that incomplete file in IndexedDB. A transient truncation becomes a sticky corrupted cache entry for later loads. Let this path throw so the retry loop can recover instead.

💡 Proposed fix
-      return loaded === total ? result.buffer : result.slice(0, loaded).buffer;
+      if (loaded !== total) {
+        throw new Error(
+          `Truncated response while fetching ${url}: received ${loaded} of ${total} bytes`
+        );
+      }
+      return result.buffer;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/packages/annotation/src/providers/modelCache.ts` at line 90, The code in
modelCache.ts currently returns a partial buffer when a stream is truncated
(returning result.slice(0, loaded).buffer) which causes corrupted cache entries;
change the logic in the function that reads weights (the loadModelWeights /
buffer-assembly code using variables result, loaded, and total) to throw an
Error when loaded !== total instead of returning a sliced buffer—include context
in the error message (e.g., expected total vs loaded bytes and model id) so the
retry loop can detect and recover from truncated reads.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/packages/annotation/src/providers/modelCache.test.ts`:
- Around line 10-45: The mock writes persist even when a readwrite transaction
is supposed to abort; change the mock so mockObjectStore.put does not write
directly to the shared `store` but instead stages the value on the transaction,
and only commit staged writes to `store` inside the transaction's oncomplete
setter (in mockDB.transaction). Ensure when `shouldFailPut` causes tx.onerror to
fire the staged writes are discarded so aborted transactions do not modify
`store`; update references in mockObjectStore.put, mockDB.transaction
(transaction, objectStore, set oncomplete) and any staging container so tests
with failOn === "put" reflect real IndexedDB semantics.

In `@app/packages/annotation/src/providers/modelCache.ts`:
- Around line 55-57: Replace the fixed 60s deadline with an idle timeout that is
re-armed after receiving headers and after each successful chunk read: create a
timeout handle (instead of a one-off setTimeout) tied to the existing
AbortController (controller), call clearTimeout and reset the timeout after
response.headers are processed and again inside the streaming loop after each
chunk is appended, and only call controller.abort() when the idle timer fires.
Also ensure the stream handler throws an error (and does not return/allow
caching) when the stream ends prematurely by checking if loaded < total and
rejecting/throwing instead of returning a partial buffer so truncated downloads
are not treated as successful.

---

Duplicate comments:
In `@app/packages/annotation/src/providers/modelCache.ts`:
- Line 90: The code in modelCache.ts currently returns a partial buffer when a
stream is truncated (returning result.slice(0, loaded).buffer) which causes
corrupted cache entries; change the logic in the function that reads weights
(the loadModelWeights / buffer-assembly code using variables result, loaded, and
total) to throw an Error when loaded !== total instead of returning a sliced
buffer—include context in the error message (e.g., expected total vs loaded
bytes and model id) so the retry loop can detect and recover from truncated
reads.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0a8ac14f-4585-4d76-8203-9fcf70ce882f

📥 Commits

Reviewing files that changed from the base of the PR and between 309e62a and 43ae192.

📒 Files selected for processing (2)
  • app/packages/annotation/src/providers/modelCache.test.ts
  • app/packages/annotation/src/providers/modelCache.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants