Skip to content

Refactor: Treat local store as a cache, never overwriting DB on sync#6

Open
threehymns wants to merge 5 commits intomainfrom
feat/local-store-as-cache-rebased
Open

Refactor: Treat local store as a cache, never overwriting DB on sync#6
threehymns wants to merge 5 commits intomainfrom
feat/local-store-as-cache-rebased

Conversation

@threehymns
Copy link
Copy Markdown
Owner

@threehymns threehymns commented May 20, 2025

I've modified SyncManager.sync to prevent the automatic upsert of all local items during the sync process. Now, sync primarily fetches remote changes to update the local cache.

New, locally-created items (not yet in the database) are identified as 'localOnlyItems' and are inserted into the database during the sync. Existing items that have local modifications will not have those modifications pushed to the server via the main sync operation; instead, the server's version will refresh the local cache.

This change ensures that the database remains the source of truth and the local store acts as a cache, updated by server changes either through full sync or realtime updates.

Summary by CodeRabbit

  • New Features
    • Introduced incremental pagination for remote data fetching during synchronization.
    • Added deep equality checks to accurately detect modified items.
  • Bug Fixes
    • Improved synchronization reliability by ensuring local-only items are inserted into the remote database after fetching remote data, with errors handled gracefully to allow partial sync completion.
  • Chores
    • Enhanced sync process to better merge local and remote items, reducing the chance of sync interruptions.

I've modified `SyncManager.sync` to prevent the automatic upsert of all local items during the sync process. Now, sync primarily fetches remote changes to update the local cache.

New, locally-created items (not yet in the database) are identified as 'localOnlyItems' and are inserted into the database during the sync. Existing items that have local modifications will not have those modifications pushed to the server via the main sync operation; instead, the server's version will refresh the local cache.

This change ensures that the database remains the source of truth and the local store acts as a cache, updated by server changes either through full sync or realtime updates.
@vercel
Copy link
Copy Markdown

vercel bot commented May 20, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
slate ✅ Ready (Inspect) Visit Preview 💬 Add feedback May 21, 2025 5:28pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented May 20, 2025

"""

Walkthrough

The SyncManager class was enhanced to support incremental pagination when fetching remote items and to synchronize data more granularly by categorizing items into local-only, remote-only, and modified sets. It inserts or upserts items conditionally with retry and backoff, logging failures without throwing, and returns a merged array of all synchronized items. A new deep equality check utility was added.

Changes

File(s) Change Summary
src/services/SyncManager.ts Added Identifiable interface and generic constraint; refactored sync to fetch remote items in paginated form, categorize items by presence and modification, insert local-only items and upsert modified items conditionally with retries and backoff, log failures without throwing, and return a merged comprehensive result. Method signatures unchanged.
src/lib/utils.ts Added deepEqual function for deep structural equality checks, handling primitives, objects, built-in types, and cyclic references.

Poem

In the warren of code, we sync with care,
Paging through data, we find what's rare.
Local or remote, we sort with grace,
Insert and update, at a steady pace.
Deep checks ensure no detail is missed—
A bunny's sync, now perfectly kissed!
🐇📦✨
"""

✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown
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)
src/services/SyncManager.ts (3)

30-33: Avoid any; provide a typed projection to satisfy ESLint and future maintainability

remoteRows and remoteItems are currently typed as any[], which triggers the ESLint errors reported in the static‐analysis section. Whenever the compiler can’t reason about the data shape, downstream refactors become hazardous.

Two lightweight options:

-const remoteRows = remoteRowsData || [];
-const remoteItems = (remoteRows as any[]).map(r => this.cfg.mapRow(r));
-const remoteIds = new Set(remoteItems.map((item: any) => (item as any).id));
+type RemoteRow = Record<string, unknown>; // or define a generated type for the table
+const remoteRows: RemoteRow[] = (remoteRowsData ?? []) as RemoteRow[];
+const remoteItems = remoteRows.map(row => this.cfg.mapRow(row));
+
+type IdOf<T> = T extends { id: infer K } ? K : never;
+const remoteIds = new Set<IdOf<typeof remoteItems[number]>>(
+  remoteItems.map(item => (item as unknown as { id: IdOf<typeof item> }).id)
+);

Or, if you can generate table-row types from Supabase, import those instead of the hand-rolled Record<string, unknown>.

This keeps the compiler happy and prevents silent type regressions down the road.

🧰 Tools
🪛 ESLint

[error] 31-31: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 32-32: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 32-32: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


34-36: Hard-coded id assumption limits reuse

localOnlyItems are detected via (item as any).id.
If a consumer supplies items whose primary key is not literally id, this silently breaks.

Consider extending SyncConfig with an idSelector: (item: Local) => string | number (defaulting to the current behaviour). That removes the hidden coupling and makes the class fully reusable across tables with composite or differently-named keys.

🧰 Tools
🪛 ESLint

[error] 35-35: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


38-52: Optional: use upsert with minimal columns to avoid race conditions

If multiple clients can create the same item offline (e.g., deterministic UUIDs), an insert call will conflict once the first succeeds.
A conservative alternative is:

.from(this.cfg.table)
.upsert(payload, { onConflict: "id" })

Passing only the columns needed for creation (id, user_id, created_at) minimises the “overwrite-remote” risk while eliminating duplicate-key failures. Evaluate whether this matches your data-model guarantees.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between d756984 and f70e8aa.

📒 Files selected for processing (1)
  • src/services/SyncManager.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/services/SyncManager.ts (1)
src/services/noteService.ts (1)
  • retryWithBackoff (15-28)
🪛 ESLint
src/services/SyncManager.ts

[error] 31-31: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 32-32: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 32-32: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 35-35: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

and swallow after the retries are exhausted

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/services/SyncManager.ts (3)

19-19: Improve comment to clarify the numbered steps

The comment is good for documenting the synchronization flow, but consider expanding it to summarize what all 4 steps do, since you've numbered them. This would give developers a quick understanding of the overall process.

- // 1. Fetch remote items (optionally incremental) - (existing code)
+ // 1. Fetch remote items (optionally incremental by timestamp)
+ // 2. Detect local-only items (items not yet on the server)
+ // 3. Insert local-only items into the remote database 
+ // 4. Return combined list of remote items and local-only items

60-63: Document the behavior change in the method's JSDoc comment

The significant change in synchronization behavior (treating local as cache, not pushing local modifications) should be documented in the method's JSDoc comment to help future developers understand the design decision.

/**
 * Sync localItems to remote and fetch remote items.
+ * 
+ * This method treats the local store as a cache. It fetches remote items and 
+ * only sends local-only items (new items) to the server. Local modifications to
+ * existing items are not pushed to the server during sync - the server remains
+ * the source of truth.
 *
 * @param since ISO timestamp to filter remote updates (inclusive)
 */

31-32: Address ESLint warnings about any types

There are several ESLint warnings about using any types in this code. Consider adding more specific types to improve type safety.

-const remoteItems = (remoteRows as any[]).map(r => this.cfg.mapRow(r));
-const remoteIds = new Set(remoteItems.map((item: any) => (item as any).id));
+// Define a type for the raw database rows
+type RemoteRow = Record<string, unknown> & { id: string };
+
+const remoteItems = (remoteRows as RemoteRow[]).map(r => this.cfg.mapRow(r));
+const remoteIds = new Set(remoteItems.map(item => item.id));
🧰 Tools
🪛 ESLint

[error] 31-31: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 32-32: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 32-32: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between f70e8aa and 8e0603c.

📒 Files selected for processing (1)
  • src/services/SyncManager.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/services/SyncManager.ts (1)
src/services/noteService.ts (1)
  • retryWithBackoff (15-28)
🪛 ESLint
src/services/SyncManager.ts

[error] 31-31: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 32-32: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 32-32: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 35-35: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🔇 Additional comments (2)
src/services/SyncManager.ts (2)

27-30: Improved error handling and defensive programming

Good improvements in error handling by explicitly throwing fetch errors and providing a fallback to an empty array when data is null. This makes the code more robust.


39-57: Fixed retry mechanism correctly implements previous review feedback

The implementation now correctly allows retryWithBackoff to retry failed insertions by:

  1. Throwing the error inside the callback
  2. Only catching and logging after all retries are exhausted

This is exactly what was suggested in the previous review and properly addresses the issue.

Copy link
Copy Markdown
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

🧹 Nitpick comments (2)
src/services/SyncManager.ts (2)

10-10: Type safety improvement needed for mapRow.

The mapRow function uses the any type which bypasses TypeScript's type checking. Consider using a more specific type that represents the structure of the rows returned from Supabase.

- mapRow: (row: any) => Local;
+ mapRow: (row: Record<string, unknown>) => Local;
🧰 Tools
🪛 ESLint

[error] 10-10: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


64-67: Document the implications of returning local-only items.

The approach of returning both remote items and local-only items (even failed ones) ensures data consistency between sync operations, but may not be immediately clear to other developers.

Consider adding JSDoc comments that explain this pattern and its implications more explicitly:

/**
 * Sync localItems to remote and fetch remote items.
 *
 * @param since ISO timestamp to filter remote updates (inclusive)
+ * @returns Array of items from the server, plus any local-only items.
+ *          Note: Local modifications to items that exist remotely will NOT be
+ *          propagated to the server. The remote version will take precedence.
 */
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 8e0603c and 4a7d75d.

📒 Files selected for processing (1)
  • src/services/SyncManager.ts (1 hunks)
🧰 Additional context used
🪛 ESLint
src/services/SyncManager.ts

[error] 10-10: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🔇 Additional comments (8)
src/services/SyncManager.ts (8)

4-6: Good addition of the Identifiable interface.

Creating this interface ensures type safety by formalizing the requirement for objects to have an id property. This is a solid foundation for the type constraints used throughout the file.


8-8: Good update to the generic constraint.

Updating SyncConfig to require Local to extend Identifiable improves type safety and makes the interface requirements explicit.


15-15: Good update to the class generic constraint.

Using the Identifiable constraint on the SyncManager class ensures that all operations can safely use the id property without type casting.


23-67: Sync logic refactoring aligns with the PR objectives.

The refactored sync logic properly treats the local store as a cache by:

  1. Fetching remote items
  2. Identifying local-only items
  3. Inserting only new local items
  4. Returning a merged list that prioritizes remote data

This implementation ensures the server remains the source of truth while still preserving new local items.


35-36: Improvements in remote items handling.

You've improved the code by safely handling the case where remoteRowsData might be null, and by directly using the typed id property now that Local extends Identifiable.


39-39: Type safety improvement in localOnlyItems filtering.

The previous code used a type cast to any to access the id property. Now with the Identifiable constraint, you can safely access item.id directly, which is more type-safe.


43-61: Proper implementation of error handling with retries.

Your implementation correctly addresses the previous review comment about retry behavior:

  1. Errors are now thrown inside the retry callback, allowing retryWithBackoff to retry
  2. The error is only caught and logged after all retries are exhausted
  3. The sync process continues even if inserts fail

This maintains non-blocking behavior while giving transient errors a chance to clear up through retries.


65-66: Good inline documentation of the caching strategy.

The comments clearly explain the caching strategy where the local cache is updated with server data plus locally created items. This helps future developers understand the design decision.

Copy link
Copy Markdown
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 (5)
src/services/SyncManager.ts (5)

83-84: Consider a more type-safe approach for dynamic property access.

Using as any for dynamic property access bypasses TypeScript's type checking. Consider creating a type-safe utility for this pattern.

-        const { [this.cfg.updatedAtColumn || 'updatedAt']: localUpdatedAt, ...localWithoutTs } = localItem as any;
-        const { [this.cfg.updatedAtColumn || 'updatedAt']: remoteUpdatedAt, ...remoteWithoutTs } = remoteItem as any;
+        // Type-safe dynamic property access
+        type ItemWithTimestamp = Local & Record<string, unknown>;
+        const timestampKey = this.cfg.updatedAtColumn || 'updatedAt';
+        
+        const localUpdatedAt = (localItem as ItemWithTimestamp)[timestampKey];
+        const { [timestampKey]: _, ...localWithoutTs } = localItem as ItemWithTimestamp;
+        
+        const remoteUpdatedAt = (remoteItem as ItemWithTimestamp)[timestampKey];
+        const { [timestampKey]: __, ...remoteWithoutTs } = remoteItem as ItemWithTimestamp;
🧰 Tools
🪛 ESLint

[error] 83-83: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 84-84: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


130-165: Robust handling of modified items with proper conflict resolution.

The modified items handling implements a "last-write-wins" strategy based on timestamps, with proper error handling and retries.

Consider adding documentation about what happens when the local timestamp is older than the remote one (currently, local changes are discarded in favor of remote data).

           // Only update if local changes are actually newer or if there are content changes
+          // If local changes are older, they will be discarded and the remote version will be used
           if (isLocalNewer) {
🧰 Tools
🪛 ESLint

[error] 145-145: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


145-145: Type-unsafe property assignment.

The use of as any for dynamic property assignment bypasses TypeScript's type checking.

-                (local as any)[this.cfg.updatedAtColumn] = now;
+                // Type-safe dynamic property assignment
+                (local as Record<string, unknown>)[this.cfg.updatedAtColumn] = now;
🧰 Tools
🪛 ESLint

[error] 145-145: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


11-11: Consider using a more specific type for mapRow parameter.

Using any for the row type reduces type safety. Consider using a generic type parameter or a more specific type that represents the database row structure.

-  mapRow: (row: any) => Local;
+  mapRow: (row: Record<string, unknown>) => Local;
🧰 Tools
🪛 ESLint

[error] 11-11: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


32-32: Consider using a more specific type for remote rows.

Using any[] for allRemoteRows reduces type safety. Consider using a more specific type that represents the structure of the database rows.

-    let allRemoteRows: any[] = [];
+    let allRemoteRows: Record<string, unknown>[] = [];
🧰 Tools
🪛 ESLint

[error] 32-32: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 4a7d75d and 3555600.

📒 Files selected for processing (2)
  • src/lib/utils.ts (1 hunks)
  • src/services/SyncManager.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/services/SyncManager.ts (2)
src/lib/utils.ts (1)
  • deepEqual (48-83)
src/services/noteService.ts (1)
  • retryWithBackoff (15-28)
🪛 ESLint
src/services/SyncManager.ts

[error] 11-11: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 32-32: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 83-83: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 84-84: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 145-145: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🔇 Additional comments (6)
src/lib/utils.ts (1)

48-83: Well-implemented deep equality function.

The deepEqual function provides robust deep comparison with comprehensive handling of various data types, cyclic references, built-in objects, and nested structures. This will improve the accuracy of object comparisons compared to simple serialization approaches.

src/services/SyncManager.ts (5)

5-7: Good addition of the Identifiable interface.

Creating a reusable interface that enforces the presence of an id property improves type safety throughout the codebase.


9-19: Well-documented pagination support.

The addition of the optional pageSize parameter with clear documentation improves the API's flexibility and helps users understand the performance implications.

🧰 Tools
🪛 ESLint

[error] 11-11: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


29-64: Effective implementation of incremental pagination.

The implementation correctly handles cursor-based pagination, ensuring all remote items are fetched efficiently without unnecessary data transfer. The approach of fetching pages until no more data remains is robust.

🧰 Tools
🪛 ESLint

[error] 32-32: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


111-128: Proper error handling with retry logic.

The implementation correctly throws errors inside the retry callback to trigger retries, and then catches them after all retries are exhausted. This addresses a previous issue where errors were swallowed inside the callback, preventing retry attempts.


167-183: Comprehensive merging strategy for synchronized items.

The merging logic effectively combines remote-only, local-only, modified, and unchanged items while avoiding duplicates. The use of a Set for tracking processed IDs is efficient.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/services/SyncManager.ts (3)

29-64: Well-implemented incremental pagination

The pagination logic is well-structured and robust, handling edge cases properly:

  • Uses configurable page size
  • Implements cursor-based pagination
  • Handles empty results
  • Detects end of data
  • Properly accumulates results

Consider adding a type for the remote rows instead of using any[]:

-let allRemoteRows: any[] = [];
+type RemoteRow = Record<string, unknown>;
+let allRemoteRows: RemoteRow[] = [];
🧰 Tools
🪛 ESLint

[error] 32-32: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


83-84: Consider improving type safety in destructuring

The as any casts could be avoided by using a more specific type or interface.

-    const { [this.cfg.updatedAtColumn || 'updatedAt']: localUpdatedAt, ...localWithoutTs } = localItem as any;
-    const { [this.cfg.updatedAtColumn || 'updatedAt']: remoteUpdatedAt, ...remoteWithoutTs } = remoteItem as any;
+    type ItemWithTimestamp = Local & Record<string, unknown>;
+    const updatedAtKey = this.cfg.updatedAtColumn || 'updatedAt';
+    const { [updatedAtKey]: localUpdatedAt, ...localWithoutTs } = localItem as ItemWithTimestamp;
+    const { [updatedAtKey]: remoteUpdatedAt, ...remoteWithoutTs } = remoteItem as ItemWithTimestamp;
🧰 Tools
🪛 ESLint

[error] 83-83: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 84-84: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


130-165: Well-implemented conflict resolution for modified items

The implementation follows the proper last-write-wins strategy:

  • Only updates if local changes are newer
  • Sets the updated timestamp on both the payload and local object
  • Uses proper error handling with retry

Consider adding a type for the updates array:

-          const updates = [];
+          const updates: Record<string, unknown>[] = [];
🧰 Tools
🪛 ESLint

[error] 145-145: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 3555600 and e7608d1.

📒 Files selected for processing (1)
  • src/services/SyncManager.ts (1 hunks)
🧰 Additional context used
🪛 ESLint
src/services/SyncManager.ts

[error] 11-11: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 32-32: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 83-83: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 84-84: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 145-145: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🔇 Additional comments (3)
src/services/SyncManager.ts (3)

3-3: Good addition of the deepEqual import

Using a dedicated deep equality function is much better than the previous approach, especially for complex nested objects.


111-128: Excellent error handling with retryWithBackoff

This properly fixes the issue identified in the previous review by:

  1. Throwing the error inside the callback to trigger retry
  2. Catching only after all retries are exhausted
  3. Logging a warning instead of failing the sync

167-183: Clean and comprehensive result merging

The merging logic properly:

  • Combines remote-only and local-only items
  • Selects the newer version of modified items
  • Includes unchanged items while avoiding duplicates

This ensures a complete and accurate sync result.

const localTimeRaw = localUpdatedAt ? new Date(localUpdatedAt).getTime() : Number.NEGATIVE_INFINITY;
const localTime = Number.isNaN(localTimeRaw) ? Number.NEGATIVE_INFINITY : localTimeRaw;
const remoteTimeRaw = remoteUpdatedAt ? new Date(remoteUpdatedAt).getTime() : Number.NEGATIVE_INFINITY;
const remoteTime = Number.isFinite(remoteTimeRaw) ? remoteTimeRaw : Number.NEGATIVE_INFINITY;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Bug: Incorrect ternary expression for remoteTime calculation

The current expression always evaluates to a boolean instead of the timestamp value.

-          const remoteTime = Number.isFinite(remoteTimeRaw) ? Number.isFinite(remoteTimeRaw) : Number.NEGATIVE_INFINITY;
+          const remoteTime = Number.isFinite(remoteTimeRaw) ? remoteTimeRaw : Number.NEGATIVE_INFINITY;
📝 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
const remoteTime = Number.isFinite(remoteTimeRaw) ? remoteTimeRaw : Number.NEGATIVE_INFINITY;
// before: returns a boolean instead of the timestamp
- const remoteTime = Number.isFinite(remoteTimeRaw) ? Number.isFinite(remoteTimeRaw) : Number.NEGATIVE_INFINITY;
+ const remoteTime = Number.isFinite(remoteTimeRaw) ? remoteTimeRaw : Number.NEGATIVE_INFINITY;
🤖 Prompt for AI Agents
In src/services/SyncManager.ts at line 92, the ternary expression for remoteTime
is incorrect because it uses Number.isFinite(remoteTimeRaw) directly, which
returns a boolean, causing remoteTime to be set to true or false instead of the
intended numeric timestamp. Fix this by checking if remoteTimeRaw is a finite
number and if so, assign remoteTimeRaw itself; otherwise, assign
Number.NEGATIVE_INFINITY.

Comment on lines +5 to 19
export interface Identifiable {
id: string;
}

export interface SyncConfig<Local extends Identifiable> {
table: string;
mapRow: (row: any) => Local;
mapLocal: (item: Local, userId: string) => Record<string, unknown>;
updatedAtColumn?: string;
/**
* Page size for fetching records. Defaults to 1000.
* Adjust based on your record size and performance needs.
*/
pageSize?: number;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Excellent type system improvements

Creating the Identifiable interface and constraining the generic type parameter makes the code more type-safe and self-documenting. The addition of the pageSize parameter with documentation is also a good enhancement.

There's still an any type in the mapRow function that could be improved:


🏁 Script executed:

#!/bin/bash
# Check how this function is used in practice
rg "mapRow" --type ts -A 5 -B 5 src/

Length of output: 3941


Use a strongly-typed row parameter in mapRow

While the new Identifiable constraint and pageSize docs are great, the mapRow: (row: any) => Local still relies on any, which erodes type safety. Please introduce a generic for the remote row type and replace any with a well-defined type.

• In src/services/SyncManager.ts, update the interface:

-export interface SyncConfig<Local extends Identifiable> {
-  table: string;
-  mapRow: (row: any) => Local;
+export interface SyncConfig<
+  Local extends Identifiable,
+  Remote = Record<string, unknown>
+> {
+  table: string;
+  mapRow: (row: Remote) => Local;
   mapLocal: (item: Local, userId: string) => Record<string, unknown>;
   updatedAtColumn?: string;
   pageSize?: number;
}

• Adjust existing usages:

  • src/store/noteStore.ts — replace (row: any) with (row: NoteRow) or Record<string, unknown>.
  • src/services/noteService.ts — similarly type the row parameter.

This change avoids any and makes each mapRow implementation explicitly typed.

📝 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 interface Identifiable {
id: string;
}
export interface SyncConfig<Local extends Identifiable> {
table: string;
mapRow: (row: any) => Local;
mapLocal: (item: Local, userId: string) => Record<string, unknown>;
updatedAtColumn?: string;
/**
* Page size for fetching records. Defaults to 1000.
* Adjust based on your record size and performance needs.
*/
pageSize?: number;
}
export interface Identifiable {
id: string;
}
export interface SyncConfig<
Local extends Identifiable,
Remote = Record<string, unknown>
> {
table: string;
mapRow: (row: Remote) => Local;
mapLocal: (item: Local, userId: string) => Record<string, unknown>;
updatedAtColumn?: string;
/**
* Page size for fetching records. Defaults to 1000.
* Adjust based on your record size and performance needs.
*/
pageSize?: number;
}
🧰 Tools
🪛 ESLint

[error] 11-11: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
In src/services/SyncManager.ts lines 5 to 19, the mapRow function parameter is
typed as any, reducing type safety. Introduce a new generic type parameter for
the remote row type in the SyncConfig interface and replace the any type in
mapRow with this generic type. Then update all existing usages of SyncConfig,
such as in src/store/noteStore.ts and src/services/noteService.ts, to specify
the appropriate row type instead of any, ensuring each mapRow implementation
uses a strongly typed parameter.

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