Skip to content

PE-9129: Sync and login resiliency quick wins - #2162

Open
vilenarios wants to merge 29 commits into
devfrom
fix/sync-quick-wins
Open

PE-9129: Sync and login resiliency quick wins#2162
vilenarios wants to merge 29 commits into
devfrom
fix/sync-quick-wins

Conversation

@vilenarios

@vilenarios vilenarios commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

First pass of the sync/data-fetching remediation plan: the highest-impact, lowest-risk fixes identified in the end-to-end sync analysis. Each change is a separate commit with its own tests; none alters happy-path behavior.

Bug fixes

  • Login no longer fails or hangs when the balance endpoint is downgetUser awaited getWalletBalance with no retry/timeout, locking out users with valid passwords during gateway blips. Balance is now best-effort (5s timeout, falls back to 0); ArDriveAuth._updateBalance refreshes it asynchronously right after login as before.
  • Wallets with 200+ drives no longer crash every synctransactionParseBatchSize computed 200 ~/ drivesCount, which reaches 0 at 201 drives and makes BatchProcessor throw. Extracted calculateTransactionParseBatchSize clamped to ≥ 1.
  • BulkImportResult.importedFiles was always empty — WorkerPool discards the execute callback's return value; successful imports are now collected in the callback.

Performance

  • Drive syncs bounded to 5 concurrent (was: every drive at once) via a new runBoundedWorkers helper mirroring Future.wait(eagerError: false) semantics — per-drive error handling, cancellation, and progress reporting unchanged.
  • MetadataCache constructed once instead of rebuilt from SharedPreferences on every parsed batch of every drive.
  • Pin transaction info fetched 100 ids per request instead of 5 — added first: 100 to the InfoOfTransactionsToBePinned query (it previously relied on the gateway's default page size, which is why the batch had to stay tiny), matching the TransactionStatuses/LicenseAssertions pattern.

Test plan

  • 13 new unit tests (batch-size clamp edge cases, balance-fetch failure paths, worker-pool bounds/failure isolation)
  • flutter analyze: clean
  • Full main-app suite on Flutter 3.19.6: 709 passed / 3 skipped / 0 failed
  • Generated artemis code is gitignored; CI regenerates via scr setup

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a Refresh list action to reload the available gateways.
    • Introduced cached/persisted gateway handling for improved availability.
    • Added configurable drive sync concurrency and drive-history pagination tuning.
  • Bug Fixes
    • Improved sync robustness with bounded concurrent processing and safer pagination/cursor handling.
    • File import results now correctly report imported successes and record per-file failures.
    • Made wallet-balance login retrieval best-effort with a timeout fallback.
    • Updated configuration upgrades to preserve intentional gateway selections.
  • Tests
    • Expanded coverage for gateway caching, sync batching, and pagination/refresh behaviors.

vilenarios and others added 7 commits July 2, 2026 23:52
…ches

- extract calculateTransactionParseBatchSize helper from syncAllDrives
- clamp result to at least 1: wallets with 200+ drives previously computed
  a batch size of 0, causing BatchProcessor to throw and every drive sync
  to fail
- guard the denominator so drivesSynced >= drivesCount cannot divide by zero
- add unit tests covering the budget split and both clamp edge cases

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- getUser previously awaited getWalletBalance with no retry, timeout, or
  error handling, so a transient gateway failure locked the user out even
  though the password was correct and the profile is stored locally
- balance fetch now falls back to zero on error or after a 5s timeout;
  ArDriveAuth._updateBalance already refreshes it asynchronously right
  after login and pushes the updated user to the auth stream
- healthy-gateway behavior is unchanged (fresh balance at login)
- deduplicate the double wallet.getAddress() call
- add tests covering sync and async balance-fetch failures

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- syncAllDrives previously started every drive's full sync pipeline at
  once via Future.wait; accounts with many drives fanned out dozens of
  concurrent snapshot/GraphQL/data-fetch pipelines and could trip gateway
  rate limits
- introduce runBoundedWorkers, a small worker-pool helper mirroring
  Future.wait(eagerError: false) semantics: all tasks run even if some
  fail, first error is reported after all complete
- per-drive error handling, cancellation, and progress reporting are
  unchanged (the per-drive closure body is untouched)
- add kMaxConcurrentDriveSyncs constant and unit tests for the pool
  (bounded in-flight count, failure isolation, edge cases)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- createDriveEntityHistoryFromTransactions rebuilt MetadataCache from a
  new SharedPreferences cache store on every parsed batch of every drive
  during sync; it now lazily constructs one instance and reuses it
- behavior is otherwise unchanged: same store, same put/get semantics

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- BulkImportResult.importedFiles was always empty: WorkerPool discards
  the execute callback's return value, so the FileEntry produced by each
  successful _importFile call was never collected
- successful imports are now added to importedFiles inside the worker
  callback; no current consumer reads the field (the bloc tracks progress
  via callbacks), so this only makes the result truthful

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- getInfoOfTxsToBePinned issued one GraphQL round-trip per 5 ids; bulk
  imports of large manifests paid 20x more sequential requests than
  needed
- add first: 100 to the InfoOfTransactionsToBePinned query (previously it
  relied on the gateway's default page size, which is why the batch size
  had to stay tiny) and raise the batch size to 100, matching the
  established pattern in TransactionStatuses and LicenseAssertions
  queries
- generated artemis code is gitignored and regenerated by scr setup

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds persistent gateway caching and refresh controls, bounded concurrent drive synchronization, configurable GraphQL pagination with fallback handling, gateway-preserving config migration, and improved failure handling for imports, balances, snapshots, and metadata caching.

Changes

Gateway management

Layer / File(s) Summary
Persistent gateway cache and refresh flow
lib/services/arweave/data_gateway_fallback.dart, lib/gar/domain/repositories/gar_repository.dart, lib/gar/presentation/bloc/gar_bloc.dart, lib/gar/presentation/bloc/gar_event.dart, lib/gar/presentation/widgets/gar_modal.dart, test/gar/domain/repository/gar_repository_test.dart, test/services/arweave/data_gateway_fallback_test.dart
GAR gateways are cached in memory and persistent storage, with explicit refresh triggerable from the modal UI; the repository and BLoC coordinate refresh and load operations, and tests validate cache behavior including persistence, SDK failures, and cross-instance reuse.

Synchronization and configuration

Layer / File(s) Summary
Sync configuration and migration
lib/services/config/app_config.dart, lib/services/config/app_config.g.dart, lib/services/config/config_fetcher.dart, lib/sync/constants.dart, assets/config/dev.json, assets/config/prod.json, assets/config/staging.json, test/services/config/config_fetcher_test.dart, test/services/config/app_config_defaults_test.dart, packages/ardrive_ui/pubspec.yaml
Adds sync concurrency and GraphQL page-size fields to config with safe defaults, updates all environment files and serialization, preserves user gateway selections during config migration, and constrains the Equatable dependency to avoid deprecation.
Bounded worker pool and sync execution
lib/sync/utils/bounded_worker_pool.dart, lib/sync/domain/repositories/sync_repository.dart, lib/main.dart, test/sync/domain/sync_repository_optimization_test.dart, test/sync/domain/transaction_parse_batch_size_test.dart, test/sync/utils/bounded_worker_pool_test.dart
Introduces bounded concurrent task execution that respects configured drive sync limits, clamps transaction batch sizing to prevent division by zero, removes ARNS sync dependencies, updates folder revision ordering for out-of-order safety, and executes all drives even when some fail.

GraphQL history retrieval

Layer / File(s) Summary
Drive-history pagination and fallback
lib/services/arweave/graphql/queries/InfoOfTransactionsToBePinned.graphql, lib/services/arweave/graphql/queries/DriveEntityHistoryWithEntityTypeFilter.graphql, lib/services/arweave/get_segmented_transaction_from_drive_strategy.dart, lib/utils/graphql_retry.dart, lib/services/arweave/arweave_service.dart, test/services/arweave/get_segmented_transaction_from_drive_strategy_test.dart
Adds configurable GraphQL pagination via page-size variables, implements three-phase fallback endpoint ladder (primary at configured size, primary at fallback size, fallback endpoint) with session-scoped owner preferences, deduplicates transactions across phase restarts, handles gateway clamping/misreporting, and provides detailed test coverage for all pagination phases and error conditions.

Runtime reliability

Layer / File(s) Summary
Runtime failure handling and cache reuse
lib/sync/data/snapshot_validation_service.dart, lib/core/arfs/use_cases/bulk_import_files.dart, lib/user/repositories/user_repository.dart, lib/authentication/ardrive_auth.dart, test/user/repositories/user_repository_test.dart
Caches snapshot-validation gateways and metadata to avoid repeated fetches, records bulk-import successes and failures in result objects, makes login wallet balance retrieval best-effort with timeout and zero fallback, and updates balance refresh to be async-aware.

Estimated code review effort: 5 (Critical) | ~100 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant GarModal as Gateway Modal
  participant GarBloc
  participant GarRepository
  participant DataGatewayFallback
  participant KeyValueStore
  participant ArioSDK
  User->>GarModal: Select Refresh list
  GarModal->>GarBloc: Dispatch RefreshGateways
  GarBloc->>GarRepository: refreshGateways()
  GarRepository->>DataGatewayFallback: refreshGateways()
  DataGatewayFallback->>ArioSDK: fetch gateways
  ArioSDK-->>DataGatewayFallback: gateway list
  DataGatewayFallback->>KeyValueStore: persist JSON
  DataGatewayFallback-->>GarRepository: updated list
  GarRepository-->>GarBloc: GatewaysLoaded
  GarBloc-->>GarModal: render refreshed gateways
Loading

Possibly related PRs

  • ardriveapp/ardrive-web#2130: Introduces new DataGatewayFallback persistent caching behavior that this PR directly integrates.
  • ardriveapp/ardrive-web#2132: Modifies GraphQLRetry fallback endpoint logic and ArweaveService GraphQL wiring that this PR builds upon for pagination.
  • ardriveapp/ardrive-web#2136: Implements bounded concurrent drive synchronization that this PR refactors into the main sync executor.
🚥 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 is related to the main changes, covering sync and login resiliency improvements, though it is broad rather than specific.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/sync-quick-wins

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.

- equatable 2.1.0 (released after the last green dev build) deprecates
  EquatableMixin, which data_table.dart uses; package lockfiles are not
  committed, so CI's per-package pub get floated to 2.1.0 and scr test
  failed on the analyze step for every PR and dev push
- the main app's committed lockfile resolves equatable 2.0.7, where
  Equatable cannot be used as a mixin, so migrating the code instead of
  pinning would break the app build; pin until both contexts can move to
  2.1.x together

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown

Visit the preview URL for this PR (updated for commit 916216e):

https://ardrive-web--pr2162-fix-sync-quick-wins-20z4ixcp.web.app

(expires Wed, 05 Aug 2026 18:24:38 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

Sign: a224ebaee2f0939e7665e7630e7d3d6cd7d0f8b0

vilenarios and others added 2 commits July 9, 2026 10:51
- switch defaultArweaveGatewayUrl from the ardrive.net proxy to
  turbo-gateway.com in all three flavors; ardrive.net proxies to
  turbo-gateway anyway and its proxy pool served 503s during the
  2026-07-08 outage, taking primary GraphQL down with it
- bump configVersion 2 -> 3 so existing users' stored configs are
  replaced with the new default on next load
- change the GraphQLRetry fallback from arweave.net/graphql to the
  Goldsky search index it proxies to (arweave-search.goldsky.com),
  avoiding arweave.net's aggressive rate limiting on the fallback path
- document Goldsky's page-size behavior: requests above 100 items are
  silently clamped with hasNextPage falsely reporting false, so fallback
  queries must never exceed 100 per page

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- equatable 2.1.0 (released after the last green dev build) deprecates
  EquatableMixin, which data_table.dart uses; package lockfiles are not
  committed, so CI's per-package pub get floated to 2.1.0 and scr test
  failed on the analyze step for every PR and dev push
- the main app's committed lockfile resolves equatable 2.0.7, where
  Equatable cannot be used as a mixin, so migrating the code instead of
  pinning would break the app build; pin until both contexts can move to
  2.1.x together

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vilenarios

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@vilenarios

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your plan includes PR reviews subject to rate limits. More reviews will be available in 54 minutes.

vilenarios and others added 3 commits July 9, 2026 13:42
- every sync (auto-sync every ~5 minutes, tab-focus restarts, manual
  syncs) forced a full ArNS sweep on Solana via
  getAntRecordsForWallet(update: true), bypassing the repository's
  15-minute cache; each sweep makes several RPC calls per owned name
- ArDrive currently has no ArNS integration, so the sweep and the
  post-sync saveAllFilesWithAssignedNames pass were pure RPC cost with
  no user-facing effect; both are removed along with the
  ARNSRepository dependency on SyncRepository
- ArNS lookups elsewhere (upload flows, profile name) are untouched and
  fetch on demand; sync-time integration can be reintroduced later if
  the feature returns

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- the AR.IO gateway list was fetched from Solana RPC once per app
  session (data-fetch fallback, snapshot validation) and on every open
  of the gateway settings modal; the registry rarely changes, so this
  was recurring RPC cost for static data
- DataGatewayFallback now persists the list in local storage: memory ->
  persisted cache -> single SDK fetch (persisted on success); across
  sessions the network is hit at most once ever
- gateway settings serve the cached list; an explicit 'Refresh list'
  action (new RefreshGateways event + modal button) force-fetches and
  persists
- SnapshotValidationService reads through the same shared cache
- corrupt or missing persisted entries fall back to a normal fetch;
  fetch failures are cached in memory only (never persisted) so the
  next session retries
- add DataGatewayFallback persistence tests and update gar repository
  tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- equatable 2.1.0 (released after the last green dev build) deprecates
  EquatableMixin, which data_table.dart uses; package lockfiles are not
  committed, so CI's per-package pub get floated to 2.1.0 and scr test
  failed on the analyze step for every PR and dev push
- the main app's committed lockfile resolves equatable 2.0.7, where
  Equatable cannot be used as a mixin, so migrating the code instead of
  pinning would break the app build; pin until both contexts can move to
  2.1.x together

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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: 2

🧹 Nitpick comments (3)
lib/services/arweave/data_gateway_fallback.dart (1)

64-87: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Guard against concurrent duplicate SDK fetches in getGatewaysCached().

The memory-cache check (cachedGateways != null) and the assignment (cachedGateways = fetched) are separated by await points. Two concurrent callers (e.g., _buildClientList during sync and SnapshotValidationService running in parallel) can both pass the null check and both trigger _arioSDK.getGateways(), violating the "at most once ever" contract and issuing duplicate Solana RPC calls.

Memoize the in-flight fetch with a Future so all concurrent callers share a single SDK call:

♻️ Proposed refactor: Future-based memoization
+  Future<List<Gateway>>? _gatewaysFuture;
+
   Future<List<Gateway>> getGatewaysCached() async {
     if (cachedGateways != null) return cachedGateways!;
+    _gatewaysFuture ??= _fetchAndCacheGateways();
+    return _gatewaysFuture!;
+  }
+
+  Future<List<Gateway>> _fetchAndCacheGateways() async {
     final persisted = await _loadPersistedGateways();
     if (persisted != null) {
       cachedGateways = persisted;
       return persisted;
     }
-
     try {
       final fetched = await _arioSDK
           .getGateways()
           .timeout(_garListTimeout, onTimeout: () => <Gateway>[]);
       cachedGateways = fetched;
       if (fetched.isNotEmpty) {
         await _persistGateways(fetched);
       }
     } catch (e) {
       logger.w('GAR list unavailable, will not retry this session: $e');
       cachedGateways = [];
     }
     return cachedGateways!;
   }
🤖 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 `@lib/services/arweave/data_gateway_fallback.dart` around lines 64 - 87,
getGatewaysCached() in data_gateway_fallback.dart can race because
cachedGateways is checked before an await and assigned later, so concurrent
callers may each call _arioSDK.getGateways(). Add in-flight memoization with a
shared Future so only one fetch runs at a time and all callers await the same
result; keep the existing persisted-cache and timeout/persist behavior, but
route the fetch through a single memoized path using getGatewaysCached(),
_loadPersistedGateways(), and _persistGateways().
lib/gar/domain/repositories/gar_repository.dart (1)

24-35: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the unused arioSDK dependency from GarRepositoryImpl. The implementation no longer references it, so the constructor and call sites can drop the parameter and simplify wiring.

🤖 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 `@lib/gar/domain/repositories/gar_repository.dart` around lines 24 - 35,
GarRepositoryImpl still accepts an unused arioSDK dependency, so remove it from
the class wiring and constructor. Update GarRepositoryImpl to drop the ArioSDK
field and required parameter, then adjust any instantiations/call sites to stop
passing it so the repository only depends on the services it actually uses.
lib/services/arweave/arweave_service.dart (1)

155-166: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Race condition in lazy _metadataCache initialization under concurrent sync.

_metadataCache ??= await ... is not atomic across an await boundary. With the new 5-concurrent-worker drive sync, two concurrent calls to createDriveEntityHistoryFromTransactions can both see _metadataCache as null, both create separate MetadataCache instances, and the second overwrites the first. Any data put into the discarded cache is lost, causing redundant metadata re-fetches on subsequent batches — partially defeating the goal of reusing a single cache.

Cache the initialization Future itself so concurrent callers share the same in-flight creation:

♻️ Suggested fix
-  MetadataCache? _metadataCache;
-
-  Future<MetadataCache> _getMetadataCache() async {
-    _metadataCache ??= await MetadataCache.fromCacheStore(
-      await newSharedPreferencesCacheStore(),
-    );
-    return _metadataCache!;
-  }
+  Future<MetadataCache>? _metadataCacheFuture;
+
+  Future<MetadataCache> _getMetadataCache() {
+    return _metadataCacheFuture ??= () async {
+      final cache = await MetadataCache.fromCacheStore(
+        await newSharedPreferencesCacheStore(),
+      );
+      return cache;
+    }();
+  }
🤖 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 `@lib/services/arweave/arweave_service.dart` around lines 155 - 166, The lazy
_metadataCache initialization in _getMetadataCache is racy because the
null-coalescing assignment spans an await, so concurrent calls can create and
overwrite separate MetadataCache instances. Change _getMetadataCache to cache
the in-flight initialization Future (or otherwise synchronize initialization) so
all callers share the same first creation, and ensure
createDriveEntityHistoryFromTransactions always receives the same MetadataCache
instance even under concurrent sync.
🤖 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 `@lib/services/arweave/data_gateway_fallback.dart`:
- Around line 91-98: Add a timeout to refreshGateways() because it currently
awaits _arioSDK.getGateways() directly and can hang indefinitely. Update the
fetch path in refreshGateways() to use the same _garListTimeout behavior as
getGatewaysCached(), so stalled AR.IO/Solana RPC calls fail fast instead of
leaving the gateway refresh stuck in LoadingGateways; keep the existing cache
update and _persistGateways flow in place after the timed fetch succeeds.

In `@test/services/arweave/data_gateway_fallback_test.dart`:
- Around line 165-193: The `DataGatewayFallback.refreshGateways()` tests only
cover the successful refresh path; add a failure-path test where
`sdk.getGateways()` throws. Make the test assert the exception propagates from
`refreshGateways()` and that the existing cached list remains unchanged by
verifying `getGatewaysCached()` still returns the original gateway after the
failed refresh.

---

Nitpick comments:
In `@lib/gar/domain/repositories/gar_repository.dart`:
- Around line 24-35: GarRepositoryImpl still accepts an unused arioSDK
dependency, so remove it from the class wiring and constructor. Update
GarRepositoryImpl to drop the ArioSDK field and required parameter, then adjust
any instantiations/call sites to stop passing it so the repository only depends
on the services it actually uses.

In `@lib/services/arweave/arweave_service.dart`:
- Around line 155-166: The lazy _metadataCache initialization in
_getMetadataCache is racy because the null-coalescing assignment spans an await,
so concurrent calls can create and overwrite separate MetadataCache instances.
Change _getMetadataCache to cache the in-flight initialization Future (or
otherwise synchronize initialization) so all callers share the same first
creation, and ensure createDriveEntityHistoryFromTransactions always receives
the same MetadataCache instance even under concurrent sync.

In `@lib/services/arweave/data_gateway_fallback.dart`:
- Around line 64-87: getGatewaysCached() in data_gateway_fallback.dart can race
because cachedGateways is checked before an await and assigned later, so
concurrent callers may each call _arioSDK.getGateways(). Add in-flight
memoization with a shared Future so only one fetch runs at a time and all
callers await the same result; keep the existing persisted-cache and
timeout/persist behavior, but route the fetch through a single memoized path
using getGatewaysCached(), _loadPersistedGateways(), and _persistGateways().
🪄 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: 1d198ccb-b197-4b9b-a558-3e423bad735b

📥 Commits

Reviewing files that changed from the base of the PR and between 0860808 and 0215ebc.

📒 Files selected for processing (25)
  • assets/config/dev.json
  • assets/config/prod.json
  • assets/config/staging.json
  • lib/core/arfs/use_cases/bulk_import_files.dart
  • lib/gar/domain/repositories/gar_repository.dart
  • lib/gar/presentation/bloc/gar_bloc.dart
  • lib/gar/presentation/bloc/gar_event.dart
  • lib/gar/presentation/widgets/gar_modal.dart
  • lib/main.dart
  • lib/services/arweave/arweave_service.dart
  • lib/services/arweave/data_gateway_fallback.dart
  • lib/services/arweave/graphql/queries/InfoOfTransactionsToBePinned.graphql
  • lib/sync/constants.dart
  • lib/sync/data/snapshot_validation_service.dart
  • lib/sync/domain/repositories/sync_repository.dart
  • lib/sync/utils/bounded_worker_pool.dart
  • lib/user/repositories/user_repository.dart
  • lib/utils/graphql_retry.dart
  • packages/ardrive_ui/pubspec.yaml
  • test/gar/domain/repository/gar_repository_test.dart
  • test/services/arweave/data_gateway_fallback_test.dart
  • test/sync/domain/sync_repository_optimization_test.dart
  • test/sync/domain/transaction_parse_batch_size_test.dart
  • test/sync/utils/bounded_worker_pool_test.dart
  • test/user/repositories/user_repository_test.dart
💤 Files with no reviewable changes (1)
  • lib/main.dart

Comment thread lib/services/arweave/data_gateway_fallback.dart
Comment thread test/services/arweave/data_gateway_fallback_test.dart
vilenarios and others added 2 commits July 9, 2026 14:19
- refreshGateways awaited the SDK with no timeout; a stalled AR.IO/Solana
  RPC left gateway settings stuck in LoadingGateways with no recovery
  (CodeRabbit finding)
- on timeout it throws so the bloc emits GatewaysError with its retry
  action; cache and persisted list are untouched on failure
- add failure-path tests: error propagation preserves existing cache,
  stalled call throws TimeoutException

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…llback

- drive-history GraphQL pagination now requests 1000 items per page on
  the primary gateway (turbo-gateway supports it; page size becomes the
  $pageSize query variable, config-driven via driveHistoryGqlPageSize),
  cutting round-trips up to 10x since cursor pagination is inherently
  sequential
- pagination is an endpoint-sticky three-phase ladder: primary at the
  configured size, primary at 100 (rules out size-specific indexer
  failures such as turbo's rows-read limit on very large owners), then
  the Goldsky fallback at 100; a phase restarts the range and a seen-id
  set deduplicates, so cursors are never reused across gateways and the
  fallback is never asked for more than 100 items (it silently clamps
  larger pages while misreporting hasNextPage)
- owners the primary cannot serve are remembered for the session so a
  multi-drive wallet pays the probing cost once, not per drive
- clamp guard: a full page claiming hasNextPage=false when more than 100
  was requested triggers a verification page instead of being trusted
- fixes a pre-existing pagination bug: the cursor advanced from the
  ArFS-version-filtered list, so a page of only unsupported transactions
  reset the cursor to null and restarted the range
- GraphQLRetry gains allowFallback / useFallbackEndpoint so paginated
  callers control endpoint stickiness; single-shot queries are unchanged
- maxConcurrentDriveSyncs promoted from constant to AppConfig so sync
  pacing is tunable without a release; both new config fields default
  safely when absent from stored configs
- add strategy pagination tests (ladder, clamp guard, dedup, owner memo,
  cursor-from-raw-edges) and config default tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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.

🧹 Nitpick comments (3)
lib/services/arweave/arweave_service.dart (1)

155-165: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Cache the Future rather than the value to avoid a lazy-init race.

_metadataCache ??= await ... has an await gap between the null-check and the assignment. With up to 5 concurrent sync workers, multiple calls can all see _metadataCache as null, each creating a separate MetadataCache — only the last survives, and the others' in-memory entries are lost. Caching the Future itself makes the ??= assignment synchronous, so all callers share one initialization.

🔒 Proposed fix: cache the Future, not the value
-  MetadataCache? _metadataCache;
-
-  Future<MetadataCache> _getMetadataCache() async {
-    _metadataCache ??= await MetadataCache.fromCacheStore(
-      await newSharedPreferencesCacheStore(),
-    );
-    return _metadataCache!;
-  }
+  Future<MetadataCache>? _metadataCacheFuture;
+
+  Future<MetadataCache> _getMetadataCache() {
+    return _metadataCacheFuture ??= _initMetadataCache();
+  }
+
+  Future<MetadataCache> _initMetadataCache() async {
+    return MetadataCache.fromCacheStore(
+      await newSharedPreferencesCacheStore(),
+    );
+  }
🤖 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 `@lib/services/arweave/arweave_service.dart` around lines 155 - 165, Update
_metadataCache to store a Future<MetadataCache> instead of a MetadataCache
value, and in _getMetadataCache assign the initialization Future synchronously
with ??= before awaiting it. Return await _metadataCache so concurrent callers
share the same initialization and cache instance.
test/services/arweave/data_gateway_fallback_test.dart (1)

216-225: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider using FakeAsync to avoid the 5-second real-time wait.

The test waits 5 real seconds for _garListTimeout to fire. Wrapping with FakeAsync would allow instant time advancement, keeping the test fast.

⚡ Proposed refactor using FakeAsync
-    test('throws on a stalled SDK call instead of hanging', () async {
-      when(() => sdk.getGateways()).thenAnswer(
-        (_) => Completer<List<Gateway>>().future, // never completes
-      );
-
-      await expectLater(
-        fallback.refreshGateways(),
-        throwsA(isA<TimeoutException>()),
-      );
-    });
+    test('throws on a stalled SDK call instead of hanging', () {
+      when(() => sdk.getGateways()).thenAnswer(
+        (_) => Completer<List<Gateway>>().future, // never completes
+      );
+
+      fakeAsync(() {
+        expectLater(
+          fallback.refreshGateways(),
+          throwsA(isA<TimeoutException>()),
+        );
+        fakeAsync.elapse(Duration(seconds: 5));
+      });
+    });

Note: import package:fake_async/fake_async.dart and wrap with FakeAsync().run((fakeAsync) { ... }) if not using flutter_test's built-in fakeAsync helper.

🤖 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 `@test/services/arweave/data_gateway_fallback_test.dart` around lines 216 -
225, Replace the real-time timeout in the stalled SDK call test with FakeAsync,
importing its package or using the available flutter_test helper. Wrap the setup
and refresh invocation in FakeAsync, advance the fake clock beyond
_garListTimeout, and then assert the returned future throws TimeoutException
without waiting five seconds.
lib/utils/graphql_retry.dart (1)

48-58: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Reuse the fallback ArtemisClient across paginated fallback reads
_paginate(... useFallbackEndpoint: true) calls execute() once per page, and each call creates and disposes a new fallback client. Hoist that client out of the loop, or cache it on GraphQLRetry, so fallback pagination doesn’t churn a client per page.

🤖 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 `@lib/utils/graphql_retry.dart` around lines 48 - 58, Reuse a single fallback
ArtemisClient across paginated reads instead of constructing and disposing one
in every execute() call. Update GraphQLRetry pagination flow and the
useFallbackEndpoint branch in execute() to hoist or cache the client, while
preserving retry handling, disposal, and cleanup when pagination completes.
🤖 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.

Nitpick comments:
In `@lib/services/arweave/arweave_service.dart`:
- Around line 155-165: Update _metadataCache to store a Future<MetadataCache>
instead of a MetadataCache value, and in _getMetadataCache assign the
initialization Future synchronously with ??= before awaiting it. Return await
_metadataCache so concurrent callers share the same initialization and cache
instance.

In `@lib/utils/graphql_retry.dart`:
- Around line 48-58: Reuse a single fallback ArtemisClient across paginated
reads instead of constructing and disposing one in every execute() call. Update
GraphQLRetry pagination flow and the useFallbackEndpoint branch in execute() to
hoist or cache the client, while preserving retry handling, disposal, and
cleanup when pagination completes.

In `@test/services/arweave/data_gateway_fallback_test.dart`:
- Around line 216-225: Replace the real-time timeout in the stalled SDK call
test with FakeAsync, importing its package or using the available flutter_test
helper. Wrap the setup and refresh invocation in FakeAsync, advance the fake
clock beyond _garListTimeout, and then assert the returned future throws
TimeoutException without waiting five seconds.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6b7f88c6-ced6-42c1-bd60-dd56c7a187a8

📥 Commits

Reviewing files that changed from the base of the PR and between 0215ebc and 67e12aa.

📒 Files selected for processing (15)
  • assets/config/dev.json
  • assets/config/prod.json
  • assets/config/staging.json
  • lib/services/arweave/arweave_service.dart
  • lib/services/arweave/data_gateway_fallback.dart
  • lib/services/arweave/get_segmented_transaction_from_drive_strategy.dart
  • lib/services/arweave/graphql/queries/DriveEntityHistoryWithEntityTypeFilter.graphql
  • lib/services/config/app_config.dart
  • lib/services/config/app_config.g.dart
  • lib/sync/constants.dart
  • lib/sync/domain/repositories/sync_repository.dart
  • lib/utils/graphql_retry.dart
  • test/services/arweave/data_gateway_fallback_test.dart
  • test/services/arweave/get_segmented_transaction_from_drive_strategy_test.dart
  • test/services/config/app_config_defaults_test.dart
💤 Files with no reviewable changes (1)
  • lib/sync/constants.dart
✅ Files skipped from review due to trivial changes (1)
  • lib/services/config/app_config.g.dart
🚧 Files skipped from review as they are similar to previous changes (5)
  • assets/config/prod.json
  • assets/config/dev.json
  • assets/config/staging.json
  • lib/services/arweave/data_gateway_fallback.dart
  • lib/sync/domain/repositories/sync_repository.dart

vilenarios and others added 2 commits July 9, 2026 20:24
- errors from a yield*'d async* stream are delivered to the listener as
  stream events and bypass the surrounding try/catch, so the pagination
  ladder never actually fell through to its next phase; phases now
  re-yield via await-for, where stream errors throw at the await point
- fix config round-trip test to go through a real jsonEncode/jsonDecode
  cycle (AppConfig.toJson embeds SelectedGateway as an object)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes the silent-truncation family found in review: downstream, stream
completion is treated as proof of completeness (sync watermarks advance,
created snapshots claim their full range on-chain), so pagination must
never end a range quietly.

- empty page with hasNextPage=true now throws instead of breaking; the
  phase ladder retries on the next endpoint and a final failure surfaces
  as a failed drive (was: silently treated as end of data)
- null-data responses with no errors also throw instead of breaking
- clamp guard broadened: every non-empty final page of an oversized
  request gets one verification page, catching gateways that clamp to
  any size (Goldsky: 100, some forks: 10), not just >=100
- driveHistoryGqlPageSize clamped to 1..1000 at the consumption site so
  a bad config value cannot produce 'successfully empty' drives
- owners are no longer marked fallback-preferring on connectivity loss
  (offline fails every endpoint; laddering would poison the session),
  and switching GraphQL endpoints clears the owner memo
- config version bumps preserve gateway choices that differ from the
  previous defaults (deliberate user/detection choices survive the 2->3
  migration instead of being silently reset)
- folder revisions gain the same dateCreated guard files already had,
  so out-of-order arrival from phase restarts cannot regress the latest
  folder state
- refreshBalance awaits the fetch so ProfileCubit re-emits the fresh
  value on the first refresh, shortening the post-login zero-balance
  window
- bulk import records worker-stage failures in BulkImportResult.failures
  (WorkerPool passes the failed task; previously these files vanished
  from both result lists)
- GraphQLRetry caches its fallback ArtemisClient (was: one client
  construct/dispose per page on the fallback path)
- GAR gateway cache memoizes the in-flight future (concurrent first
  callers share one Solana RPC fetch) and treats an empty persisted list
  as not cached
- tests updated for verification-page semantics; new tests: sub-100
  clamp recovery, empty-page and null-data hard failures, gateway-choice
  preservation across config version bumps

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vilenarios vilenarios closed this Jul 10, 2026
@vilenarios vilenarios reopened this Jul 10, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vilenarios
vilenarios force-pushed the fix/sync-quick-wins branch from ba04082 to 6b714e4 Compare July 10, 2026 03:22
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
test/services/config/config_fetcher_test.dart (1)

158-187: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Test "replaces local config with asset config if local config has no version" is inconsistent with the new migration logic.

The oldConfig has arweaveGatewayUrl: 'old-gateway' and arweaveGatewayForDataRequest: SelectedGateway(label: 'old', url: 'old'), both differing from the previous defaults ('https://ardrive.net', 'https://turbo-gateway.com') and the new defaults ('new-gateway', 'new'). The migration logic at lines 76-88 would preserve both via copyWith, so the saved config would NOT equal newConfigString. The assertion at line 186 (verify(() => localStore.putString('config', newConfigString)).called(1)) would fail.

The updated test at lines 98-129 correctly uses any() and asserts gateway preservation — this test needs the same treatment.

🐛 Proposed fix: align test with migration behavior
       // Act
       final result = await configFetcher.fetchConfig(Flavor.development);

       // Assert
       expect(result.configVersion, 2);
       expect(result.stripePublishableKey, 'new-key');
-      verify(() => localStore.putString('config', newConfigString)).called(1);
+      expect(result.arweaveGatewayUrl, 'old-gateway');
+      expect(result.arweaveGatewayForDataRequest.url, 'old');
+      verify(() => localStore.putString('config', any())).called(1);
     });
🤖 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 `@test/services/config/config_fetcher_test.dart` around lines 158 - 187, Update
the test “replaces local config with asset config if local config has no
version” to reflect migration preservation behavior: replace the exact
newConfigString verification with a captured or any() persisted value, then
assert the saved config preserves oldConfig’s arweaveGatewayUrl and
arweaveGatewayForDataRequest while adopting the new config version and other
expected fields, matching the approach used by the updated migration test.
🧹 Nitpick comments (1)
lib/authentication/ardrive_auth.dart (1)

437-448: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consolidate _updateBalance() with refreshBalance().

The new refreshBalance() correctly awaits the balance fetch and handles errors. However, _updateBalance() (lines 375–383) duplicates the same fetch-update-emit logic in a fire-and-forget pattern and is still called from _addUser (line 368) and unlockUser (line 206). Consider replacing those _updateBalance() calls with refreshBalance() (called without await where fire-and-forget is acceptable) to eliminate the duplication and ensure consistent error handling.

♻️ Proposed consolidation
-  void _updateBalance() {
-    _userRepository.getBalance(currentUser.wallet).then((value) {
-      _currentUser = _currentUser!.copyWith(walletBalance: value);
-      _userStreamController.add(_currentUser);
-    }).catchError((e) {
-      logger.e('Error fetching wallet balance', e);
-      // Don't update balance on error - keep previous value
-    });
-  }

Then replace call sites:

-    _updateBalance();
+    refreshBalance();
🤖 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 `@lib/authentication/ardrive_auth.dart` around lines 437 - 448, Consolidate the
duplicate balance-refresh logic by removing `_updateBalance()` and using
`refreshBalance()` instead. Update the call sites in `_addUser` and `unlockUser`
to invoke `refreshBalance()` without awaiting where fire-and-forget behavior is
intended, preserving the existing sequencing while reusing its awaited fetch,
update, emit, and error handling.
🤖 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 `@lib/core/arfs/use_cases/bulk_import_files.dart`:
- Around line 421-436: The onWorkerError callback in the bulk import flow loses
the original exception and treats cancellation as a regular file failure. Update
the WorkerPool/execute error propagation so onWorkerError receives or can
retrieve the thrown exception, record that exception’s message and stack trace
in FileImportFailure and logging, and handle BulkImportException with “Bulk
import cancelled” separately rather than adding it to normal failures.

---

Outside diff comments:
In `@test/services/config/config_fetcher_test.dart`:
- Around line 158-187: Update the test “replaces local config with asset config
if local config has no version” to reflect migration preservation behavior:
replace the exact newConfigString verification with a captured or any()
persisted value, then assert the saved config preserves oldConfig’s
arweaveGatewayUrl and arweaveGatewayForDataRequest while adopting the new config
version and other expected fields, matching the approach used by the updated
migration test.

---

Nitpick comments:
In `@lib/authentication/ardrive_auth.dart`:
- Around line 437-448: Consolidate the duplicate balance-refresh logic by
removing `_updateBalance()` and using `refreshBalance()` instead. Update the
call sites in `_addUser` and `unlockUser` to invoke `refreshBalance()` without
awaiting where fire-and-forget behavior is intended, preserving the existing
sequencing while reusing its awaited fetch, update, emit, and error handling.
🪄 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: 467c123f-0fe9-4f4c-9b89-0cfc2c4453ea

📥 Commits

Reviewing files that changed from the base of the PR and between 67e12aa and 6b714e4.

📒 Files selected for processing (11)
  • lib/authentication/ardrive_auth.dart
  • lib/core/arfs/use_cases/bulk_import_files.dart
  • lib/services/arweave/arweave_service.dart
  • lib/services/arweave/data_gateway_fallback.dart
  • lib/services/arweave/get_segmented_transaction_from_drive_strategy.dart
  • lib/services/config/config_fetcher.dart
  • lib/sync/domain/repositories/sync_repository.dart
  • lib/utils/graphql_retry.dart
  • test/services/arweave/get_segmented_transaction_from_drive_strategy_test.dart
  • test/services/config/app_config_defaults_test.dart
  • test/services/config/config_fetcher_test.dart
🚧 Files skipped from review as they are similar to previous changes (5)
  • test/services/config/app_config_defaults_test.dart
  • lib/services/arweave/data_gateway_fallback.dart
  • lib/utils/graphql_retry.dart
  • lib/services/arweave/get_segmented_transaction_from_drive_strategy.dart
  • lib/sync/domain/repositories/sync_repository.dart

Comment thread lib/core/arfs/use_cases/bulk_import_files.dart Outdated
@vilenarios

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your plan includes PR reviews subject to rate limits. More reviews will be available in 35 minutes.

vilenarios and others added 5 commits July 10, 2026 06:06
… to 15

Field test on a 21-drive wallet showed the branch slower than prod
(82s vs 27s) with more GraphQL calls (120 vs 39). Two causes addressed:

- verification pages fired for every non-empty final page of an
  oversized request; 19 of the 21 drives fit in a single page, so this
  was pure overhead for small drives. The guard now skips tails smaller
  than 10 raw edges: no known gateway clamps below 10 (Goldsky: 100,
  forks: 10), so a sub-10 tail cannot be a silent clamp. The Goldsky
  clamp-lie defense is unchanged for full-looking pages.
- maxConcurrentDriveSyncs default raised 5 -> 15 (config-tunable).
  Per-drive sync is latency-bound for small drives, so 5-wide pacing
  serialized a previously fully-parallel 21-drive account into ~4 waves,
  dominating wall time. turbo comfortably served 21 concurrent
  full-range first:1000 probes in testing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field testing on a 21-drive wallet showed 1000-item pages slower
end-to-end in the app despite fast id-only probes: full
TransactionCommon fragments make a 1000-node page a multi-megabyte
response, parsed on the browser main thread, multiplied by concurrent
drive syncs. Round-trip savings did not offset payload and parse cost.

- driveHistoryGqlPageSize now defaults to 100 in the Dart model and all
  flavor configs; at 100 the wire behavior matches prod (single-phase
  pagination, no verification pages, which only apply above 100)
- the pagination machinery is unchanged and still tested at 1000: the
  endpoint-sticky fallback ladder, clamp guard, seen-id dedup, and
  cursor fixes all remain, and larger pages stay one config edit away
  if future measurements justify them

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Final review flagged a resilience regression: phase A used 2 attempts
regardless of page size, so at the default 100 a ~1s transient blip
failed over to the fallback index and memoized the owner there for the
entire session (prod rode out blips with 8 attempts of backoff).

- primary-phase attempts now scale: 2 at oversized page sizes (failures
  there are deterministic indexer limits; fast downshift is right), 8 at
  the safe page size (failures there are transient; ride them out)
- the owner fallback memo is cleared with the other sync-end caches, so
  a degraded sync heals on the next one instead of pinning the session
  to the fallback index

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field measurements showed no end-to-end win from larger GraphQL pages
(full-fragment payload and main-thread parse costs offset the saved
round-trips), so the pagination machinery is removed rather than kept
at parity: the strategy, GraphQLRetry, and the drive-history query
return to their dev-proven behavior, and driveHistoryGqlPageSize is
dropped from AppConfig and all flavor configs.

Kept from the sync work (measured or correctness wins):
- Solana RPC eliminated from sync (ArNS removal, GAR persistence)
- login balance best-effort, 200+-drive batch clamp, bulk import fixes
- GraphQL endpoints: turbo-gateway primary, Goldsky fallback (retained
  in the reverted GraphQLRetry), configVersion migration preserving
  custom gateways
- bounded drive syncs via maxConcurrentDriveSyncs config (15)
- folder revision dateCreated guard, awaited refreshBalance,
  MetadataCache reuse, GAR cache future-memoization

The pagination work (endpoint-sticky ladder, clamp-lie defenses,
cursor fixes) remains in branch history for a future data-driven
retry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 15-wide pacing split a 21-drive account into two waves, roughly
  doubling wall time vs prod's fully-parallel sync when per-drive work
  is latency-bound
- 50 syncs virtually every real account in a single wave, matching
  prod-equivalent wall time, while still bounding pathological
  many-drive wallets (pool clamp allows up to 64 via config)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vilenarios and others added 2 commits July 29, 2026 13:44
# Conflicts:
#	assets/config/dev.json
#	lib/core/arfs/use_cases/bulk_import_files.dart
… PE-9129

calculateTransactionParseBatchSize divided the 200-tx budget by every
remaining drive, but only maxConcurrentDriveSyncs drives sync at once. Once an
account exceeds the concurrency bound the batch size under-shot badly (200
drives -> batch 1 instead of ~4), needlessly slowing large-account syncs —
a case beyond what the branch was field-tested against (21 drives). Divide by
min(remaining, maxConcurrent) so each concurrently-syncing drive gets its
fair share of the budget; smaller accounts and the div-by-zero guards are
unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsV2WFHZMGD9DUfX65cxuW
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