Skip to content

Conversation

@klehmann
Copy link

@klehmann klehmann commented Nov 22, 2025

As discussed on Discord, the sync of files got broken in recent codebase changes. This change pushes locally added files to the registered remotes and uses the remotes as fallback if an attachment does not yet exist locally.

Summary by CodeRabbit

  • New Features

    • Added remote fallback loading: the system now attempts to load from remote sources when local retrieval fails, automatically caching successful remote data locally.
    • Enhanced remote data synchronization to ensure files and metadata are consistently propagated to all configured remote stores.
  • Bug Fixes

    • Improved error handling for file operations with structured logging for better troubleshooting and error context.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 22, 2025

Walkthrough

Adds structured error logging around CRDT file decoding, implements remote-loading fallback and local caching when CARs are missing, and reinstates/extends remote propagation for WAL and file upload processing with loader existence guards.

Changes

Cohort / File(s) Summary
CRDT file decoding error handling
core/base/crdt-helpers.ts
Wraps getFile calls in try/catch, logs contextual details (car, cid, errorMessage, errorStack), and throws richer structured errors containing cid, car, fileName and original error on decode failures.
Loader: remote fallback & typed byte access
core/blockstore/loader.ts
Expands active store type to include FileStore, adds remote-loading fallback when local load fails (iterates remotes, attempts load, caches remote CAR locally and updates active store), improves error handling/logging, and uses typed access to decoded bytes (bytes.value.data) ensuring consistent Uint8Array returns.
Store: WAL & remote propagation guards
core/blockstore/store.ts
Adds runtime guards for this.loader, re-enables/extends remote propagation for CARs and files (threads publicFile flag), ensures remote meta saves for lastOp, and guarantees WAL state persistence after processing attempts.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Loader
    participant Local as Local CarStore/FileStore
    participant Remote as Remote Stores
    participant Store as BlockStore
    participant CRDT as CRDT decoder

    Client->>Loader: loadCar(cid)
    Loader->>Local: load(cid)
    alt Local found
        Local-->>Loader: car bytes
        Loader->>Client: loadedCar
    else Local not found / error
        rect rgb(220,240,230)
        Note over Loader,Remote: Remote fallback
        Loader->>Remote: iterate remotes -> load(cid)
        alt Remote succeeds
            Remote-->>Loader: car bytes
            Loader->>Local: save(car)    -- cache locally
            Local-->>Loader: saved
            Loader->>Client: loadedCar
        else Remote fails (each)
            Remote-->>Loader: error
            Note over Loader: log failure, continue
        end
        end
    end

    Client->>Store: processWAL/op(file)
    Store->>Store: ensure this.loader exists (guard)
    Store->>Local: handle local save
    Store->>Remote: forRemotes: save(car/file, publicFlag)
    Remote-->>Store: ack / error (logged)
Loading
sequenceDiagram
    participant Reader
    participant CAR as CAR bytes
    participant CRDT as Decoder

    Reader->>CRDT: decodeFile(block)
    alt decode success
        CRDT-->>Reader: file bytes
    else decode throws
        CRDT->>CRDT: catch error
        CRDT->>CRDT: log {car, cid, errorMessage, errorStack}
        CRDT-->>Reader: throw structured error {cid, car, fileName, error, stacks}
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • focus areas:
    • core/blockstore/loader.ts: verify remote iteration, caching logic, active store updates, and typed bytes.value.data usage
    • core/blockstore/store.ts: check loader guards, remote propagation loops, publicFile flag propagation, and WAL persistence paths
    • core/base/crdt-helpers.ts: confirm structured logging format and that rethrown errors preserve useful context and stack traces

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes across all modified files: enabling remote storage of files, remote fetching as fallback, and proper error handling in file operations.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


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 and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
core/base/crdt-helpers.ts (1)

252-299: Improved file decode error logging looks good; consider including filename and avoiding redundant checks

The new try/catch around blocks.getFile and the richer decodeFile error path both add useful context (car, cid, message, stack, original error) and surface failures as proper logged errors, which aligns well with the remote-loading/WAL flows.

Two small optional tweaks:

  • You already guard on if (fileMeta.car) before assigning fileMeta.file, so using throwFalsy(fileMeta.car) multiple times is a bit redundant; capturing const car = throwFalsy(fileMeta.car) once and reusing it would both clarify the invariant and avoid re-throwing if car were somehow mutated later.
  • In the decode error log, fileName is populated from fileMeta.type, which is typically a MIME type rather than the logical filename. If available, logging the filename from the outer loop (or adding a dedicated fileName field in DocFileMeta) would make these logs easier to correlate to user-facing files.
core/blockstore/store.ts (1)

491-623: WAL remote propagation logic is solid; verify loader-null guard and file-only processing behavior

The refactor in _doProcess achieves the intended behavior:

  • noLoaderOps and operations now push cars to all remotes via this.loader.attachedStores.forRemotes(...), which replaces the older single xremoteCarStore path.
  • fileOperations correctly load the local file block once and then save it to each remote file store with the public flag, and prune entries from walState.fileOperations.
  • After successfully handling operations, pushing lastOp to each remote meta store via forRemotes keeps remote meta in sync.
  • Using retryableUpload with pRetry and a small concurrencyLimit is a good balance between robustness and load.

A couple of points worth double-checking:

  • this.loader is required in the constructor, so the early if (!this.loader) return; and the repeated if (!this.loader) return; checks inside each retryableUpload should never trigger in normal usage. If they do ever trigger (e.g., due to future refactors or type casts), _doProcess will exit without touching walState, while process() continues to re-schedule itself as long as there are pending operations, which could cause a silent retry loop. If you expect loader to sometimes be absent, consider logging and skipping the re-schedule in that case, or clearing walState explicitly.
  • enqueueFile only appends to this.walState.fileOperations and does not call process(). That means file uploads will be flushed to remotes only when some other code path also enqueues a non-noLoader commit (which does trigger process()). Please confirm that every caller that uses enqueueFile is paired with a corresponding enqueue/commit, or consider invoking void this.process() from enqueueFile as well if immediate propagation of standalone file uploads is important.
core/blockstore/loader.ts (1)

826-915: Remote fallback in makeDecoderAndCarReader matches intent; avoid double local save and duplicate casts

The new flow nicely covers the “missing locally, fetch from remotes, then cache locally” story:

  • Local activeStore.load(carCid) first, with non-isNotFoundError exceptions surfaced as logged errors.
  • On NotFound, you walk store.remotes(), try remote.load(carCid), and when successful, use that store’s codec via await activeStore.keyedCrypto() and mark the item as originating from the remote store.
  • Decoding through asyncBlockDecode and feeding CarReader.fromBytes from the decrypted bytes.value.data fixes the earlier mismatch where fromBytes expected raw Uint8Array car bytes.

Two refinements to consider:

  1. Avoid saving the same remote car to local twice

    In the remote-success path you currently:

    const remoteCar = await remote.load(carCid);
    if (remoteCar) {
      this.logger.Debug().Ref("cid", remoteCar.cid).Msg("saving remote car locally");
      await store.local().save(remoteCar);
      loadedCar = remoteCar;
      activeStore = remote;
      break;
    }
    ...
    if (activeStore !== store.local()) {
      await store.local().save(loadedCar);
    }

    When a remote load succeeds, store.local().save(...) is invoked both inside the loop and again in the activeStore !== store.local() block. It’s harmless but causes redundant IO. You can drop the first save and keep a single “save once” after the try/catch, moving the debug log there, for example:

    -          const remoteCar = await remote.load(carCid);
    -          if (remoteCar) {
    -            this.logger.Debug().Ref("cid", remoteCar.cid).Msg("saving remote car locally");
    -            await store.local().save(remoteCar);
    -            loadedCar = remoteCar;
    -            activeStore = remote;
    -            break;
    -          }
    +          const remoteCar = await remote.load(carCid);
    +          if (remoteCar) {
    +            loadedCar = remoteCar;
    +            activeStore = remote;
    +            break;
    +          }
    ...
    -    if (activeStore !== store.local()) {
    -      // if coming from remote store, save it locally not in cache
    -      await store.local().save(loadedCar);
    -    }
    +    if (loadedCar && activeStore !== store.local()) {
    +      this.logger.Debug().Ref("cid", loadedCar.cid).Msg("saving remote car locally");
    +      await store.local().save(loadedCar);
    +    }
  2. DRY up and type the decoded bytes access

    You access (bytes.value as { data: Uint8Array }).data twice (for CarReader.fromBytes and the returned bytes field). To make this clearer and safer, consider extracting once with a typed helper/local:

    const decoded = bytes.value as { data: Uint8Array };
    const carBytes = decoded.data;
    
    const rawReader = await CarReader.fromBytes(carBytes);
    ...
    return {
      cid: carCid,
      bytes: carBytes,
      item: { ... },
    };

    This avoids repeating the cast and makes the expected shape of bytes.value explicit.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4f344e3 and a001a5c.

📒 Files selected for processing (3)
  • core/base/crdt-helpers.ts (1 hunks)
  • core/blockstore/loader.ts (5 hunks)
  • core/blockstore/store.ts (5 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-13T14:08:24.069Z
Learnt from: mabels
Repo: fireproof-storage/fireproof PR: 801
File: src/blockstore/loader.ts:401-403
Timestamp: 2025-05-13T14:08:24.069Z
Learning: In the `handleMetaStream` function in `src/blockstore/loader.ts`, recursive calls intentionally omit the `opts` parameter to avoid calling the `.first` callback multiple times, even though this also means the error handler won't be called for subsequent read errors.

Applied to files:

  • core/base/crdt-helpers.ts
🧬 Code graph analysis (3)
core/base/crdt-helpers.ts (1)
core/types/base/types.ts (1)
  • throwFalsy (71-76)
core/blockstore/store.ts (1)
core/blockstore/loader.ts (1)
  • carLogIncludesGroup (55-68)
core/blockstore/loader.ts (1)
core/types/blockstore/types.ts (2)
  • CarStore (462-467)
  • FileStore (469-474)
🪛 ESLint
core/blockstore/store.ts

[error] 32-32: 'throwFalsy' is defined but never used. Allowed unused vars must match /^_/u.

(@typescript-eslint/no-unused-vars)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)
core/blockstore/store.ts (1)

470-474: Critical: Missing walState save in enqueueFile may cause data loss.

Unlike the enqueue method (line 464), enqueueFile does not save the walState after modifying it. This creates a window where fileOperations could be lost if the process crashes before _doProcess executes and saves the state (line 621).

For consistency and durability, enqueueFile should persist the walState immediately after modification, matching the pattern in enqueue.

Apply this diff to ensure durability:

 async enqueueFile(fileCid: AnyLink, publicFile = false) {
   await this.ready();
   this.walState.fileOperations.push({ cid: fileCid, public: publicFile });
-  // await this.save(this.walState)
+  await this.save(this.walState);
 }
🧹 Nitpick comments (1)
core/blockstore/store.ts (1)

522-524: Consider adding debug logging to loader existence guards.

The loader existence checks are good defensive programming and prevent potential null/undefined errors. However, the silent early returns could make debugging harder if the loader is unexpectedly missing during processing.

Consider adding debug logging to help diagnose issues:

 if (!this.loader) {
+  this.logger.Debug().Msg("Loader not available, skipping processing");
   return;
 }

Also applies to: 551-553, 579-581, 603-605

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a001a5c and 3d0b2ec.

📒 Files selected for processing (1)
  • core/blockstore/store.ts (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-29T11:19:21.299Z
Learnt from: mabels
Repo: fireproof-storage/fireproof PR: 801
File: src/use-fireproof/index.ts:19-22
Timestamp: 2025-05-29T11:19:21.299Z
Learning: In src/use-fireproof/index.ts, the defaultChanged function intentionally throws an error as part of a lazy initialization pattern. This error should only occur if the lazy logic fails to properly replace the default handler with actual event handlers during setup. The error serves as a fail-fast mechanism to catch initialization problems early.

Applied to files:

  • core/blockstore/store.ts
🧬 Code graph analysis (1)
core/blockstore/store.ts (1)
core/blockstore/loader.ts (1)
  • carLogIncludesGroup (55-68)
🔇 Additional comments (4)
core/blockstore/store.ts (4)

532-536: LGTM! Remote propagation for noLoaderOps aligns with PR objectives.

The addition of remote propagation logic ensures that locally available cars are pushed to all registered remote stores. This implementation correctly handles the case where cars exist locally and need to be synchronized.


560-565: LGTM! Remote propagation for operations implemented correctly.

The remote propagation logic for regular operations mirrors the noLoaderOps implementation and correctly ensures cars are synchronized to all remote stores.


577-577: LGTM! Public flag handling for file operations is correct.

The implementation correctly destructures the public flag from file operations and passes it through to the remote save operations, enabling proper access control for files stored in remotes.

Also applies to: 586-589


606-609: LGTM! Remote metadata save completes the synchronization flow.

The remote metadata save is correctly positioned after all car and file uploads complete, ensuring remote stores receive consistent metadata updates. This completes the remote synchronization implementation as described in the PR objectives.

@mabels
Copy link
Contributor

mabels commented Nov 23, 2025

Thanks for submitting this --- there are some conceptual things which you missed --- It would be cool if I could explain them to you --- pls contact me on Discord @fastandfearless or fast&fear<.
We should be in the same timezone and on the same language, DE.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants