Skip to content

Refactor virtual drive sync engine#405

Open
egalvis27 wants to merge 3 commits into
mainfrom
refactor/sync-engine-process
Open

Refactor virtual drive sync engine#405
egalvis27 wants to merge 3 commits into
mainfrom
refactor/sync-engine-process

Conversation

@egalvis27

@egalvis27 egalvis27 commented Jul 3, 2026

Copy link
Copy Markdown

What is Changed / Added


  • Decoupled virtual-drive mount from the initial full sync, so mount starts immediately after DB initialization.
  • Introduced lazy, on-demand hydration for directory/path lookups instead of preloading the full tree.
  • Optimized remote sync flow to process data in smaller batches and in deterministic order (folders first, then files), with default filtering focused on existing items.
  • Improved SQLite performance and stability for sync-heavy paths:
    • enabled WAL and tuned pragmas/indexes for read/write contention,
    • kept batch upserts transactional,
    • removed parallel transaction collisions by executing folder/file batch writes sequentially in lazy hydration.
  • Added directory state caching in SQLite (freshness markers) to avoid unnecessary refetches during repeated navigation.
  • Updated virtual-drive operation handlers (opendir, getattr, open, release) to work with lazy hydration and the new sync behavior.
  • Aligned tests for startup, virtual-drive operations, and remote sync manager with the new sync strategy.

Why

  • The previous approach depended too much on large upfront synchronization, which delayed mount and degraded responsiveness on large accounts.
  • Lazy hydration reduces startup work and shifts data loading to real user navigation paths.
  • Smaller, ordered background batches reduce memory pressure and make sync progress more predictable.
  • SQLite tuning and transaction sequencing prevent lock/transaction errors under concurrent sync activity.
  • The overall result is faster mount time, smoother directory browsing, and a more resilient sync pipeline under real workload.

Summary by CodeRabbit

  • New Features

    • Virtual drive now loads directories and paths lazily on demand, improving responsiveness when opening folders/files.
    • Offline drive now supports a queued background upload flow for temporal files, improving upload reliability.
  • Bug Fixes

    • Remote sync now fetches smaller batches, syncs folders and files sequentially, and better filters for existing items.
    • Improved virtual drive SQLite initialization and directory-state recovery during startup and after remote changes.
    • Strengthened local hydration for open, opendir, and getAttributes so paths resolve more consistently.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 67959794-7d49-44c2-8657-238e38dda907

📥 Commits

Reviewing files that changed from the base of the PR and between ea64034 and 07c52fa.

📒 Files selected for processing (2)
  • .eslintrc.js
  • src/infra/local-file-system/safe-readdir.test.ts
💤 Files with no reviewable changes (1)
  • src/infra/local-file-system/safe-readdir.test.ts
✅ Files skipped from review due to trivial changes (1)
  • .eslintrc.js

📝 Walkthrough

Walkthrough

This PR adds lazy virtual-drive hydration with SQLite-backed directory state, switches temporal uploads to an async queue, updates remote-sync fetch behavior and limits, and adds SQLite bootstrap initialization and transactional batch writes.

Changes

Virtual drive hydration, upload queue, and sync flow

Layer / File(s) Summary
Repository contracts and staging
src/context/virtual-drive/files/domain/FileRepository.ts, .../InMemoryFileRepository.ts, src/context/virtual-drive/folders/domain/FolderRepository.ts, .../InMemoryFolderRepository.ts, .../FolderRepositoryMock.ts, src/context/storage/TemporalFiles/domain/TemporalFileRepository.ts, .../NodeTemporalFileRepository.ts
Adds deleteMatchingPartial to file and folder repositories and mocks, and adds stage to temporal-file repository implementations.
SQLite bootstrap and transactional upserts
src/apps/main/database/data-source.ts, src/core/bootstrap/register-session-event-handlers.ts, src/infra/sqlite/services/file/create-or-update-file-by-batch.ts, src/infra/sqlite/services/folder/create-or-update-folder-by-batch.ts
Adds virtual-drive SQLite bootstrap statements and initializer wiring, and wraps file/folder batch upserts in transactions.
Directory state repository
src/backend/features/virtual-drive/services/lazy/DirectoryStateSqliteRepository.ts
Adds SQLite-backed freshness tracking for folder children by folder id and status scope.
Lazy virtual-drive hydrator
src/backend/features/virtual-drive/services/lazy/LazyVirtualDriveHydrator.ts
Adds lazy folder materialization, directory refresh, local persistence, and path-loading helpers.
DI wiring for hydrator and repositories
src/apps/drive/dependency-injection/virtual-drive/*
Registers the lazy hydrator and directory-state repository, and removes private visibility from file/folder repository bindings.
FUSE operations use lazy hydration
src/backend/features/virtual-drive/services/operations/get-attributes.service.ts, .../open.service.ts, .../opendir.service.ts, .../*.test.ts
Updates attribute lookup, open, and opendir flows to use the lazy hydrator and refreshes the related tests.
Virtual drive startup and sync reset
src/backend/features/virtual-drive/services/drive-folder/virtual-drive.service.ts, src/backend/features/virtual-drive/ipc/handlers.ts, .../*.test.ts
Seeds the root folder, clears directory state on startup and after remote sync, and switches the startup trigger event.
Temporal upload queue and release flow
src/context/storage/TemporalFiles/application/upload/TemporalFileUploadQueue.ts, src/backend/features/virtual-drive/services/operations/release.service.ts, src/apps/drive/dependency-injection/offline-drive/registerTemporalFilesServices.ts, .../*.test.ts, src/core/electron/paths.ts
Adds an async temporal-file upload queue, wires it into DI, changes release to enqueue uploads, and adds the upload queue path.
Remote sync fetch and ordering
src/apps/main/remote-sync/RemoteSyncManager.ts, service.ts, src/context/virtual-drive/remoteTree/infrastructure/SQLiteRemoteItemsGenerator.ts, .../*.test.ts
Changes remote sync ordering, status filters, request limits, and remote item field fallback mapping.
Supporting fixes
src/context/virtual-drive/folders/application/create/SimpleFolderCreator.ts, vitest.setup.main.ts, src/infra/local-file-system/safe-readdir.test.ts, .eslintrc.js
Updates an already-exists error check, adds an environment mock export, tightens readdir typing, and disables no-await-in-loop.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant FuseOperation
  participant LazyVirtualDriveHydrator
  participant FolderRepository
  participant DirectoryStateSqliteRepository
  participant ServerAPI
  FuseOperation->>LazyVirtualDriveHydrator: readDirectory(path)
  LazyVirtualDriveHydrator->>FolderRepository: ensureFolderMaterialized(path)
  LazyVirtualDriveHydrator->>DirectoryStateSqliteRepository: isFresh(folderId, scope)
  alt stale
    LazyVirtualDriveHydrator->>ServerAPI: fetchFolder(folderId)
    ServerAPI-->>LazyVirtualDriveHydrator: children DTOs
    LazyVirtualDriveHydrator->>FolderRepository: upsert children
    LazyVirtualDriveHydrator->>DirectoryStateSqliteRepository: markLoaded/markError
  end
  LazyVirtualDriveHydrator-->>FuseOperation: local directory entries
Loading
sequenceDiagram
  participant ReleaseService
  participant TemporalFileUploadQueue
  participant TemporalFileRepository
  participant Uploader
  ReleaseService->>TemporalFileUploadQueue: enqueue(temporalFile, path, processName)
  TemporalFileUploadQueue->>TemporalFileRepository: stage(path, targetFolder)
  TemporalFileUploadQueue->>Uploader: run(task)
  Uploader-->>TemporalFileUploadQueue: success or error
  TemporalFileUploadQueue-->>ReleaseService: queued (async)
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change set around virtual drive sync and hydration refactoring.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 refactor/sync-engine-process

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/backend/features/virtual-drive/ipc/handlers.ts (1)

14-24: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle the fire-and-forget failure path. void Promise.all([...]) drops rejections from either async call, so a failure in updateVirtualDriveContainer or DirectoryStateSqliteRepository.clear() becomes an unhandled promise rejection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/features/virtual-drive/ipc/handlers.ts` around lines 14 - 24, The
fire-and-forget logic in remoteChangesSyncedHandler currently ignores failures
from both updateVirtualDriveContainer and
DirectoryStateSqliteRepository.clear(), which can surface as unhandled
rejections. Update this handler to explicitly catch and log errors from the
Promise.all work, using the existing remoteChangesSyncedHandler,
updateVirtualDriveContainer, and DirectoryStateSqliteRepository.clear symbols so
the failure path is handled safely without dropping rejected promises.
🧹 Nitpick comments (5)
src/backend/features/virtual-drive/services/operations/opendir.service.test.ts (1)

57-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test only covers rejection with an already-well-formed FuseError.

This masks the cast issue in opendir.service.ts (error: err as FuseError) — add a case where hydrator.readDirectory rejects with a plain Error to verify the service still returns a valid FuseError with a correct .code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/backend/features/virtual-drive/services/operations/opendir.service.test.ts`
around lines 57 - 59, The existing opendir service test only exercises a
rejection that is already a FuseError, so it does not catch the unsafe cast in
opendir.service.ts. Extend the test in opendir.service.test.ts around
hydrator.readDirectory to also reject with a plain Error and assert the service
still resolves to a FuseError with the expected code. Use the opendir service
path and the FuseError/FuseCodes assertions to verify the fallback behavior when
err is not already a FuseError.
src/backend/features/virtual-drive/services/operations/get-attributes.service.ts (1)

86-118: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate file/folder attribute-mapping logic.

The hydrated-entry file/folder attribute blocks (Lines 89-117) closely mirror the earlier local-entry blocks (Lines 54-82). Consider extracting toFileAttributes(file) / toFolderAttributes(folder) helpers to avoid drift between the two near-identical mappings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/backend/features/virtual-drive/services/operations/get-attributes.service.ts`
around lines 86 - 118, The file and folder attribute mapping in
GetAttributesService is duplicated between the local-entry and hydrated-entry
branches, which risks drift over time. Refactor the shared logic into reusable
helpers such as toFileAttributes and toFolderAttributes (or equivalent methods
near findLocalEntry / ensurePathLoaded) and have both branches call them so the
mode, size, timestamps, uid/gid, and nlink mapping stays consistent in one
place.
src/backend/features/virtual-drive/services/operations/get-attributes.service.test.ts (1)

75-84: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing coverage for hydrated-entry early-return branches.

The new logic in getAttributes (implementation Lines 86-117) re-resolves findLocalEntry after hydration and returns early if a file/folder is now found — this is the core new behavior for this layer. This test only exercises the path where hydration finds nothing and falls back to document. Add cases where fileSearcher.run/folderSearcher.run resolve to a value only after hydrator.ensurePathLoaded is called, to verify the hydrated early-return paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/backend/features/virtual-drive/services/operations/get-attributes.service.test.ts`
around lines 75 - 84, The current getAttributes test only covers the fallback
path after hydration, but it misses the new early-return behavior after
re-running findLocalEntry. Add test cases in get-attributes.service.test.ts
where hydrator.ensurePathLoaded is invoked and then fileSearcher.run or
folderSearcher.run resolves to a hydrated file/folder, and assert getAttributes
returns those attributes immediately instead of continuing to document fallback.
Use the existing getAttributes, hydrator.ensurePathLoaded, fileSearcher.run, and
folderSearcher.run symbols to target the new hydrated-entry branches.
src/context/storage/TemporalFiles/application/upload/TemporalFileUploadQueue.ts (1)

12-22: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Props and UploadTask are structurally identical.

Consider reusing one type to avoid duplication as the shape evolves.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/context/storage/TemporalFiles/application/upload/TemporalFileUploadQueue.ts`
around lines 12 - 22, Props and UploadTask are duplicated with the same shape,
so reuse a single type definition in TemporalFileUploadQueue to avoid drift as
fields change. Update the queue’s type declarations so one of the symbols, Props
or UploadTask, becomes the shared source of truth and the other references it
directly.
src/apps/drive/dependency-injection/virtual-drive/registerFolderServices.ts (1)

28-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The // TODO: can be private? comment on line 28 is now stale — this binding is intentionally made non-private so the lazy hydrator can resolve FolderRepository. Consider removing the TODO to avoid confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/apps/drive/dependency-injection/virtual-drive/registerFolderServices.ts`
around lines 28 - 30, The TODO comment in registerFolderServices is stale
because the FolderRepository binding is intentionally public for lazy hydrator
resolution. Remove the “can be private?” comment from the
builder.register(FolderRepository).use(InMemoryFolderRepository).asSingleton()
registration so the intent is clear and there’s no confusion for future readers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/apps/main/database/data-source.ts`:
- Around line 41-49: `initializeVirtualDriveSqlite()` is currently
all-or-nothing, so a single failing bootstrap statement can stop startup and
prevent the `APP_DATA_SOURCE_INITIALIZED` emit from happening. Update this
helper to make `SQLITE_BOOTSTRAP_STATEMENTS` best-effort by handling errors per
statement inside the loop, logging failures with context via
`AppDataSource.query`, and continuing instead of throwing so the virtual-drive
startup path can proceed.

In
`@src/backend/features/virtual-drive/services/lazy/LazyVirtualDriveHydrator.ts`:
- Around line 51-52: The `readLocalDirectory` call in `LazyVirtualDriveHydrator`
is passing an unused `statusScope` argument, which is misleading and flagged by
lint. Remove the `statusScope` parameter from the `readLocalDirectory` signature
and update the `LazyVirtualDriveHydrator` call site (and any other referenced
overloads around the same code path) so `folder` is the only needed argument,
keeping the behavior unchanged.
- Around line 79-96: The loop in LazyVirtualDriveHydrator is intentionally
sequential, so the no-await-in-loop lint warning should be handled explicitly
rather than left unresolved. Add a targeted lint disable comment on the await
inside the path-segment loop in LazyVirtualDriveHydrator, and include a brief
justification that each segment depends on the previous folder being hydrated
before continuing.
- Around line 127-196: The delete order in fetchAndStoreChildren is unsafe
because folderRepository.deleteMatchingPartial and
fileRepository.deleteMatchingPartial run before the new local children are fully
persisted. Update fetchAndStoreChildren to write the replacement folders/files
first using createOrUpdateFolderByBatch, createOrUpdateFileByBatch, and the
Promise.all add/upsert step, then remove stale rows afterward. Keep the existing
mapping logic and error handling, but make sure the durable insert/update
completes before any delete calls.

In
`@src/context/storage/TemporalFiles/application/upload/TemporalFileUploadQueue.ts`:
- Around line 40-56: The deduplication in TemporalFileUploadQueue.enqueue is
vulnerable to a race because queuedPaths is only updated after the await on
repository.stage, allowing duplicate uploads for the same path. Reserve the path
in queuedPaths before calling repository.stage in enqueue, then proceed to stage
and push the task using the existing enqueue, drain, and queuedPaths symbols so
concurrent calls can no longer pass the has check before the path is marked.

In `@src/context/virtual-drive/files/infrastructure/InMemoryFileRepository.ts`:
- Around line 107-118: deleteMatchingPartial in InMemoryFileRepository should
use the same contentsId normalization behavior as matchingPartial, since strict
equality can miss entries that should match. Update the comparison logic inside
deleteMatchingPartial to special-case contentsId the same way matchingPartial
does, while keeping the rest of the key checks unchanged, so the two methods
stay consistent.

---

Outside diff comments:
In `@src/backend/features/virtual-drive/ipc/handlers.ts`:
- Around line 14-24: The fire-and-forget logic in remoteChangesSyncedHandler
currently ignores failures from both updateVirtualDriveContainer and
DirectoryStateSqliteRepository.clear(), which can surface as unhandled
rejections. Update this handler to explicitly catch and log errors from the
Promise.all work, using the existing remoteChangesSyncedHandler,
updateVirtualDriveContainer, and DirectoryStateSqliteRepository.clear symbols so
the failure path is handled safely without dropping rejected promises.

---

Nitpick comments:
In `@src/apps/drive/dependency-injection/virtual-drive/registerFolderServices.ts`:
- Around line 28-30: The TODO comment in registerFolderServices is stale because
the FolderRepository binding is intentionally public for lazy hydrator
resolution. Remove the “can be private?” comment from the
builder.register(FolderRepository).use(InMemoryFolderRepository).asSingleton()
registration so the intent is clear and there’s no confusion for future readers.

In
`@src/backend/features/virtual-drive/services/operations/get-attributes.service.test.ts`:
- Around line 75-84: The current getAttributes test only covers the fallback
path after hydration, but it misses the new early-return behavior after
re-running findLocalEntry. Add test cases in get-attributes.service.test.ts
where hydrator.ensurePathLoaded is invoked and then fileSearcher.run or
folderSearcher.run resolves to a hydrated file/folder, and assert getAttributes
returns those attributes immediately instead of continuing to document fallback.
Use the existing getAttributes, hydrator.ensurePathLoaded, fileSearcher.run, and
folderSearcher.run symbols to target the new hydrated-entry branches.

In
`@src/backend/features/virtual-drive/services/operations/get-attributes.service.ts`:
- Around line 86-118: The file and folder attribute mapping in
GetAttributesService is duplicated between the local-entry and hydrated-entry
branches, which risks drift over time. Refactor the shared logic into reusable
helpers such as toFileAttributes and toFolderAttributes (or equivalent methods
near findLocalEntry / ensurePathLoaded) and have both branches call them so the
mode, size, timestamps, uid/gid, and nlink mapping stays consistent in one
place.

In
`@src/backend/features/virtual-drive/services/operations/opendir.service.test.ts`:
- Around line 57-59: The existing opendir service test only exercises a
rejection that is already a FuseError, so it does not catch the unsafe cast in
opendir.service.ts. Extend the test in opendir.service.test.ts around
hydrator.readDirectory to also reject with a plain Error and assert the service
still resolves to a FuseError with the expected code. Use the opendir service
path and the FuseError/FuseCodes assertions to verify the fallback behavior when
err is not already a FuseError.

In
`@src/context/storage/TemporalFiles/application/upload/TemporalFileUploadQueue.ts`:
- Around line 12-22: Props and UploadTask are duplicated with the same shape, so
reuse a single type definition in TemporalFileUploadQueue to avoid drift as
fields change. Update the queue’s type declarations so one of the symbols, Props
or UploadTask, becomes the shared source of truth and the other references it
directly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 522a22bd-87c6-469c-a716-8b578d062dac

📥 Commits

Reviewing files that changed from the base of the PR and between e6b1171 and ea64034.

📒 Files selected for processing (37)
  • src/apps/drive/dependency-injection/offline-drive/registerTemporalFilesServices.ts
  • src/apps/drive/dependency-injection/virtual-drive/registerFilesServices.ts
  • src/apps/drive/dependency-injection/virtual-drive/registerFolderServices.ts
  • src/apps/drive/dependency-injection/virtual-drive/registerVirtualDriveSharedServices.ts
  • src/apps/main/database/data-source.ts
  • src/apps/main/remote-sync/RemoteSyncManager.test.ts
  • src/apps/main/remote-sync/RemoteSyncManager.ts
  • src/apps/main/remote-sync/service.ts
  • src/backend/features/virtual-drive/ipc/handlers.ts
  • src/backend/features/virtual-drive/services/drive-folder/virtual-drive.service.test.ts
  • src/backend/features/virtual-drive/services/drive-folder/virtual-drive.service.ts
  • src/backend/features/virtual-drive/services/lazy/DirectoryStateSqliteRepository.ts
  • src/backend/features/virtual-drive/services/lazy/LazyVirtualDriveHydrator.ts
  • src/backend/features/virtual-drive/services/operations/get-attributes.service.test.ts
  • src/backend/features/virtual-drive/services/operations/get-attributes.service.ts
  • src/backend/features/virtual-drive/services/operations/open.service.test.ts
  • src/backend/features/virtual-drive/services/operations/open.service.ts
  • src/backend/features/virtual-drive/services/operations/opendir.service.test.ts
  • src/backend/features/virtual-drive/services/operations/opendir.service.ts
  • src/backend/features/virtual-drive/services/operations/release.service.test.ts
  • src/backend/features/virtual-drive/services/operations/release.service.ts
  • src/context/storage/TemporalFiles/application/upload/TemporalFileUploadQueue.ts
  • src/context/storage/TemporalFiles/domain/TemporalFileRepository.ts
  • src/context/storage/TemporalFiles/infrastructure/NodeTemporalFileRepository.ts
  • src/context/virtual-drive/files/domain/FileRepository.ts
  • src/context/virtual-drive/files/infrastructure/InMemoryFileRepository.ts
  • src/context/virtual-drive/folders/__mocks__/FolderRepositoryMock.ts
  • src/context/virtual-drive/folders/application/create/SimpleFolderCreator.ts
  • src/context/virtual-drive/folders/domain/FolderRepository.ts
  • src/context/virtual-drive/folders/infrastructure/InMemoryFolderRepository.ts
  • src/context/virtual-drive/remoteTree/infrastructure/SQLiteRemoteItemsGenerator.ts
  • src/core/bootstrap/register-session-event-handlers.ts
  • src/core/electron/paths.ts
  • src/infra/local-file-system/safe-readdir.test.ts
  • src/infra/sqlite/services/file/create-or-update-file-by-batch.ts
  • src/infra/sqlite/services/folder/create-or-update-folder-by-batch.ts
  • vitest.setup.main.ts

Comment on lines +41 to +49
export async function initializeVirtualDriveSqlite() {
if (!AppDataSource.isInitialized) {
return;
}

for (const statement of SQLITE_BOOTSTRAP_STATEMENTS) {
await AppDataSource.query(statement);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bootstrap failure will block virtual-drive startup. initializeVirtualDriveSqlite() has no error handling, and in the login flow it is awaited before eventBus.emit('APP_DATA_SOURCE_INITIALIZED') (see register-session-event-handlers.ts line 33-34). A single failing PRAGMA/CREATE INDEX (e.g. an index referencing a not-yet-synced column) throws, the emit is skipped, and the virtual drive mount trigger — which this stack switches to APP_DATA_SOURCE_INITIALIZED — never fires.

Index/pragma tuning is best-effort; wrap each statement so a failure is logged but non-fatal.

🛡️ Make bootstrap best-effort
   for (const statement of SQLITE_BOOTSTRAP_STATEMENTS) {
-    await AppDataSource.query(statement);
+    try {
+      await AppDataSource.query(statement);
+    } catch (error) {
+      logger.error({ msg: 'Error running SQLite bootstrap statement', statement, error });
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function initializeVirtualDriveSqlite() {
if (!AppDataSource.isInitialized) {
return;
}
for (const statement of SQLITE_BOOTSTRAP_STATEMENTS) {
await AppDataSource.query(statement);
}
}
export async function initializeVirtualDriveSqlite() {
if (!AppDataSource.isInitialized) {
return;
}
for (const statement of SQLITE_BOOTSTRAP_STATEMENTS) {
try {
await AppDataSource.query(statement);
} catch (error) {
logger.error({ msg: 'Error running SQLite bootstrap statement', statement, error });
}
}
}
🧰 Tools
🪛 GitHub Actions: Lint / 🔍 Lint

[warning] 47-47: ESLint no-await-in-loop: Unexpected await inside a loop.

🪛 GitHub Actions: Lint / 0_🔍 Lint.txt

[warning] 47-47: ESLint warning: Unexpected await inside a loop. (no-await-in-loop)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/apps/main/database/data-source.ts` around lines 41 - 49,
`initializeVirtualDriveSqlite()` is currently all-or-nothing, so a single
failing bootstrap statement can stop startup and prevent the
`APP_DATA_SOURCE_INITIALIZED` emit from happening. Update this helper to make
`SQLITE_BOOTSTRAP_STATEMENTS` best-effort by handling errors per statement
inside the loop, logging failures with context via `AppDataSource.query`, and
continuing instead of throwing so the virtual-drive startup path can proceed.

Comment on lines +51 to +52

return readLocalDirectory({ folder, statusScope });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the unused statusScope param from readLocalDirectory.

Flagged by pipeline lint as unused. Currently harmless since DirectoryStatusScope has a single member, but it misleadingly implies scope-based filtering that isn't implemented.

🧹 Proposed fix
-    return readLocalDirectory({ folder, statusScope });
+    return readLocalDirectory({ folder });
...
   function readLocalDirectory({
     folder,
-    statusScope,
   }: {
     folder: Folder;
-    statusScope: DirectoryStatusScope;
   }): DirectoryEntries {

Also applies to: 208-223

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/features/virtual-drive/services/lazy/LazyVirtualDriveHydrator.ts`
around lines 51 - 52, The `readLocalDirectory` call in
`LazyVirtualDriveHydrator` is passing an unused `statusScope` argument, which is
misleading and flagged by lint. Remove the `statusScope` parameter from the
`readLocalDirectory` signature and update the `LazyVirtualDriveHydrator` call
site (and any other referenced overloads around the same code path) so `folder`
is the only needed argument, keeping the behavior unchanged.

Source: Pipeline failures

Comment on lines +79 to +96
for (const segment of requestedPath.split('/').filter(Boolean)) {
currentPath = `${currentPath}/${segment}`;

const existing = folderRepository.matchingPartial({ path: currentPath })[0];
if (existing) {
currentFolder = existing;
continue;
}

await refreshChildrenIfNeeded({ folder: currentFolder, statusScope });

const hydrated = folderRepository.matchingPartial({ path: currentPath })[0];
if (!hydrated) {
throw new FuseError(FuseCodes.ENOENT, `[FUSE - Lazy] Folder not found: ${currentPath}`);
}

currentFolder = hydrated;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Silence the no-await-in-loop warning with justification instead of leaving it unresolved.

Flagged by pipeline lint. The sequential await here is intentional — each path segment's materialization depends on the previous one being resolved — so this isn't a real parallelization opportunity; add an explicit disable comment to document that.

🧹 Proposed fix
       const existing = folderRepository.matchingPartial({ path: currentPath })[0];
       if (existing) {
         currentFolder = existing;
         continue;
       }
 
+      // eslint-disable-next-line no-await-in-loop -- each segment must be resolved sequentially before descending
       await refreshChildrenIfNeeded({ folder: currentFolder, statusScope });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const segment of requestedPath.split('/').filter(Boolean)) {
currentPath = `${currentPath}/${segment}`;
const existing = folderRepository.matchingPartial({ path: currentPath })[0];
if (existing) {
currentFolder = existing;
continue;
}
await refreshChildrenIfNeeded({ folder: currentFolder, statusScope });
const hydrated = folderRepository.matchingPartial({ path: currentPath })[0];
if (!hydrated) {
throw new FuseError(FuseCodes.ENOENT, `[FUSE - Lazy] Folder not found: ${currentPath}`);
}
currentFolder = hydrated;
}
for (const segment of requestedPath.split('/').filter(Boolean)) {
currentPath = `${currentPath}/${segment}`;
const existing = folderRepository.matchingPartial({ path: currentPath })[0];
if (existing) {
currentFolder = existing;
continue;
}
// eslint-disable-next-line no-await-in-loop -- each segment must be resolved sequentially before descending
await refreshChildrenIfNeeded({ folder: currentFolder, statusScope });
const hydrated = folderRepository.matchingPartial({ path: currentPath })[0];
if (!hydrated) {
throw new FuseError(FuseCodes.ENOENT, `[FUSE - Lazy] Folder not found: ${currentPath}`);
}
currentFolder = hydrated;
}
🧰 Tools
🪛 GitHub Actions: Lint / 0_🔍 Lint.txt

[warning] 88-88: ESLint warning: Unexpected await inside a loop. (no-await-in-loop)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/features/virtual-drive/services/lazy/LazyVirtualDriveHydrator.ts`
around lines 79 - 96, The loop in LazyVirtualDriveHydrator is intentionally
sequential, so the no-await-in-loop lint warning should be handled explicitly
rather than left unresolved. Add a targeted lint disable comment on the await
inside the path-segment loop in LazyVirtualDriveHydrator, and include a brief
justification that each segment depends on the previous folder being hydrated
before continuing.

Source: Pipeline failures

Comment on lines +127 to +196
async function fetchAndStoreChildren({ folder, statusScope }: { folder: Folder; statusScope: DirectoryStatusScope }) {
try {
const { data, error } = await fetchFolder(folder.uuid);

if (error || !data) {
throw error ?? new FolderNotFoundError(folder.path);
}

const remoteFolders = data.children.filter(isExistingFolder).map((child) => toRemoteFolder(child));
const remoteFiles = data.files.filter(isExistingFile).map((child) => toRemoteFile(child));

// SQLite (better-sqlite3) uses a single writer connection.
// Running both batch transactions in parallel causes nested transaction errors.
await createOrUpdateFolderByBatch({ folders: remoteFolders });
await createOrUpdateFileByBatch({ files: remoteFiles });
await folderRepository.deleteMatchingPartial({ parentId: folder.id });
await fileRepository.deleteMatchingPartial({ folderId: folder.id });

const localFolders = data.children.filter(isExistingFolder).map((child) => {
return createFolderFromServerFolder(
{
bucket: child.bucket ?? null,
createdAt: child.createdAt,
id: child.id,
name: child.name,
parentId: child.parentId,
updatedAt: child.updatedAt,
plain_name: child.plainName,
status: ServerFolderStatus.EXISTS,
uuid: child.uuid,
},
joinVirtualPath(folder.path, child.plainName),
);
});

const localFiles = data.files.filter(isExistingFile).map((child) => {
return createFileFromServerFile(
{
bucket: child.bucket,
createdAt: child.createdAt,
encrypt_version: child.encryptVersion,
fileId: child.fileId ?? '',
folderId: child.folderId,
id: child.id,
modificationTime: child.modificationTime ?? child.updatedAt,
name: child.name,
size: Number.parseInt(String(child.size), 10),
type: child.type,
updatedAt: child.updatedAt,
userId: child.userId,
status: ServerFileStatus.EXISTS,
plainName: child.plainName,
uuid: child.uuid,
},
joinVirtualPath(folder.path, buildFileName(child)),
);
});

await Promise.all([
...localFolders.map((child) => folderRepository.add(child)),
...localFiles.map((child) => fileRepository.upsert(child)),
]);

await directoryStateRepository.markLoaded({ folderId: folder.id, statusScope });
} catch (error) {
await directoryStateRepository.markError({ folderId: folder.id, statusScope });
logger.error({ msg: '[FUSE - Lazy] Failed to fetch folder children', error, path: folder.path });
throw new FuseError(FuseCodes.EIO, `[FUSE - Lazy] Unable to hydrate path: ${folder.path}`);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: check FolderRepository's add/upsert contract to see if a non-destructive reconciliation is possible
ast-grep run --pattern 'add($_) { $$$ }' --lang typescript src/context/virtual-drive/folders
rg -n "add\(|upsert\(" src/context/virtual-drive/folders/domain/FolderRepository.ts src/context/virtual-drive/folders/infrastructure/InMemoryFolderRepository.ts

Repository: internxt/drive-desktop-linux

Length of output: 2008


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant files first.
git ls-files | rg '^src/(backend/features/virtual-drive/services/lazy/LazyVirtualDriveHydrator\.ts|context/virtual-drive/folders/.*)$' || true

# Show the relevant sections if the files exist.
for f in \
  src/backend/features/virtual-drive/services/lazy/LazyVirtualDriveHydrator.ts \
  src/context/virtual-drive/folders/domain/FolderRepository.ts \
  src/context/virtual-drive/folders/infrastructure/InMemoryFolderRepository.ts
do
  if [ -f "$f" ]; then
    echo "===== $f ====="
    wc -l "$f"
    sed -n '1,260p' "$f"
  fi
done

Repository: internxt/drive-desktop-linux

Length of output: 1979


Avoid deleting children before the replacement set is durable
If mapping or the final Promise.all fails after these deletes, the folder is left empty in the local store until a later hydration repopulates it. Write the new entries first, then remove stale ones.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/features/virtual-drive/services/lazy/LazyVirtualDriveHydrator.ts`
around lines 127 - 196, The delete order in fetchAndStoreChildren is unsafe
because folderRepository.deleteMatchingPartial and
fileRepository.deleteMatchingPartial run before the new local children are fully
persisted. Update fetchAndStoreChildren to write the replacement folders/files
first using createOrUpdateFolderByBatch, createOrUpdateFileByBatch, and the
Promise.all add/upsert step, then remove stale rows afterward. Keep the existing
mapping logic and error handling, but make sure the durable insert/update
completes before any delete calls.

Comment on lines +40 to +56
async function enqueue({ temporalFile, path, processName }: Props) {
if (queuedPaths.has(path)) {
logger.debug({ msg: '[UploadQueue] Path already queued', path, processName });
return;
}

const stagedTemporalFile = await repository.stage(temporalFile.path, PATHS.UPLOAD_QUEUE);

tasks.push({
temporalFile: stagedTemporalFile,
path,
processName,
});
queuedPaths.add(path);

void drain();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Race condition: dedup check and queuedPaths.add are split by an await, allowing duplicate staged uploads for the same path.

queuedPaths.has(path) is checked synchronously, but queuedPaths.add(path) only happens after await repository.stage(...) resolves (Line 53). If enqueue is called twice for the same path in quick succession (e.g., rapid FUSE release events), both calls can pass the has() check before either adds to the set, resulting in two staged copies and two uploads for the same file.

🔒 Proposed fix: reserve the path before the await
   async function enqueue({ temporalFile, path, processName }: Props) {
     if (queuedPaths.has(path)) {
       logger.debug({ msg: '[UploadQueue] Path already queued', path, processName });
       return;
     }
 
-    const stagedTemporalFile = await repository.stage(temporalFile.path, PATHS.UPLOAD_QUEUE);
-
-    tasks.push({
-      temporalFile: stagedTemporalFile,
-      path,
-      processName,
-    });
-    queuedPaths.add(path);
+    queuedPaths.add(path);
+
+    try {
+      const stagedTemporalFile = await repository.stage(temporalFile.path, PATHS.UPLOAD_QUEUE);
+      tasks.push({ temporalFile: stagedTemporalFile, path, processName });
+    } catch (error) {
+      queuedPaths.delete(path);
+      throw error;
+    }
 
     void drain();
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function enqueue({ temporalFile, path, processName }: Props) {
if (queuedPaths.has(path)) {
logger.debug({ msg: '[UploadQueue] Path already queued', path, processName });
return;
}
const stagedTemporalFile = await repository.stage(temporalFile.path, PATHS.UPLOAD_QUEUE);
tasks.push({
temporalFile: stagedTemporalFile,
path,
processName,
});
queuedPaths.add(path);
void drain();
}
async function enqueue({ temporalFile, path, processName }: Props) {
if (queuedPaths.has(path)) {
logger.debug({ msg: '[UploadQueue] Path already queued', path, processName });
return;
}
queuedPaths.add(path);
try {
const stagedTemporalFile = await repository.stage(temporalFile.path, PATHS.UPLOAD_QUEUE);
tasks.push({ temporalFile: stagedTemporalFile, path, processName });
} catch (error) {
queuedPaths.delete(path);
throw error;
}
void drain();
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/context/storage/TemporalFiles/application/upload/TemporalFileUploadQueue.ts`
around lines 40 - 56, The deduplication in TemporalFileUploadQueue.enqueue is
vulnerable to a race because queuedPaths is only updated after the await on
repository.stage, allowing duplicate uploads for the same path. Reserve the path
in queuedPaths before calling repository.stage in enqueue, then proceed to stage
and push the task using the existing enqueue, drain, and queuedPaths symbols so
concurrent calls can no longer pass the has check before the path is marked.

Comment on lines +107 to +118
async deleteMatchingPartial(partial: Partial<FileAttributes>): Promise<void> {
const keys = Object.keys(partial) as Array<keyof Partial<FileAttributes>>;

for (const [uuid, attributes] of this.filesByUuid.entries()) {
const matches = keys.every((key: keyof FileAttributes) => attributes[key] === partial[key]);

if (matches) {
this.filesByUuid.delete(uuid);
this.filesByContentsId.delete(attributes.contentsId);
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Inconsistent contentsId comparison vs matchingPartial.

matchingPartial normalizes contentsId before comparing (Line 67), but deleteMatchingPartial uses strict === for all keys, including contentsId. This can cause deletions to silently miss entries that matchingPartial would otherwise match.

🐛 Proposed fix to mirror the normalize special-case
   async deleteMatchingPartial(partial: Partial<FileAttributes>): Promise<void> {
     const keys = Object.keys(partial) as Array<keyof Partial<FileAttributes>>;

     for (const [uuid, attributes] of this.filesByUuid.entries()) {
-      const matches = keys.every((key: keyof FileAttributes) => attributes[key] === partial[key]);
+      const matches = keys.every((key: keyof FileAttributes) => {
+        if (key === 'contentsId') {
+          return attributes[key].normalize() === (partial[key] as string).normalize();
+        }
+        return attributes[key] === partial[key];
+      });

       if (matches) {
         this.filesByUuid.delete(uuid);
         this.filesByContentsId.delete(attributes.contentsId);
       }
     }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async deleteMatchingPartial(partial: Partial<FileAttributes>): Promise<void> {
const keys = Object.keys(partial) as Array<keyof Partial<FileAttributes>>;
for (const [uuid, attributes] of this.filesByUuid.entries()) {
const matches = keys.every((key: keyof FileAttributes) => attributes[key] === partial[key]);
if (matches) {
this.filesByUuid.delete(uuid);
this.filesByContentsId.delete(attributes.contentsId);
}
}
}
async deleteMatchingPartial(partial: Partial<FileAttributes>): Promise<void> {
const keys = Object.keys(partial) as Array<keyof Partial<FileAttributes>>;
for (const [uuid, attributes] of this.filesByUuid.entries()) {
const matches = keys.every((key: keyof FileAttributes) => {
if (key === 'contentsId') {
return attributes[key].normalize() === (partial[key] as string).normalize();
}
return attributes[key] === partial[key];
});
if (matches) {
this.filesByUuid.delete(uuid);
this.filesByContentsId.delete(attributes.contentsId);
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context/virtual-drive/files/infrastructure/InMemoryFileRepository.ts`
around lines 107 - 118, deleteMatchingPartial in InMemoryFileRepository should
use the same contentsId normalization behavior as matchingPartial, since strict
equality can miss entries that should match. Update the comparison logic inside
deleteMatchingPartial to special-case contentsId the same way matchingPartial
does, while keeping the rest of the key checks unchanged, so the two methods
stay consistent.

@sonarqubecloud

sonarqubecloud Bot commented Jul 7, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
15.9% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

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.

1 participant