Skip to content

Conversation

@maparent
Copy link
Collaborator

@maparent maparent commented Oct 24, 2025

Summary by CodeRabbit

  • Improvements
    • Enhanced data synchronization engine with automatic retry logic and exponential backoff to gracefully handle transient failures and improve reliability.
    • Refined sync error handling with better status notifications and error messaging for improved transparency.
    • Increased system resilience against connection interruptions with improved failure detection and recovery mechanisms.

@linear
Copy link

linear bot commented Oct 24, 2025

@supabase
Copy link

supabase bot commented Oct 24, 2025

This pull request has been ignored for the connected project zytfjzqyijgagqxrzbmz because there are no changes detected in packages/database/supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@maparent
Copy link
Collaborator Author

@CodeRabbit review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 24, 2025

✅ Actions performed

Review triggered.

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.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 24, 2025

📝 Walkthrough

Walkthrough

Refactored the Supabase sync module to introduce a centralized, time-based scheduler with persistent state management. Added exponential backoff on failures with hard caps, introduced FatalError handling for unrecoverable conditions, updated type signatures with Date objects, and exposed new control functions (setSyncActivity, initializeSupabaseSync) to manage sync lifecycle and conditional toast notifications.

Changes

Cohort / File(s) Summary
Sync Scheduler & Control Flow Overhaul
apps/roam/src/utils/syncDgNodesToSupabase.ts
Replaced static constants and direct invocations with a doSync-driven lifecycle using setSyncActivity and aBASE_SYNC_INTERVAL-based timeout mechanism. Added FatalError class to abort sync on unrecoverable conditions. Introduced persistent state tracking: activeTimeout, numFailures, and cancellation logic. Added exponential backoff on failures with MAX_FAILURES hard cap. Changed DEFAULT_TIME from string to Date object. Updated imports to proper ES modules: createClient and DGSupabaseClient type; added Json, CompositeTypes, Enums type imports.
endSyncTask Signature & Behavior
apps/roam/src/utils/syncDgNodesToSupabase.ts
Updated status parameter type to Enums<"task_status">. Added optional showToast flag to conditionally render error/success toasts. Refactored to guard against missing success/failure paths and preserve previous toast content.
proposeSyncTask Return Shape
apps/roam/src/utils/syncDgNodesToSupabase.ts
Refactored lastUpdateTime from string to optional Date. Added optional nextUpdateTime Date field. Updated error return shapes to align with new timing fields, no longer returning lastUpdateTime in error cases.
New Public Exports
apps/roam/src/utils/syncDgNodesToSupabase.ts
Added setSyncActivity(active: boolean): void to control sync state. Added createOrUpdateDiscourseEmbedding(showToast?: boolean): Promise with orchestration logic that respects doSync state, validates client/context with FatalError, and uses endSyncTask with showToast. Added initializeSupabaseSync(): Promise to initialize sync by checking createClient() result and scheduling first run.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant initializeSupabaseSync
    participant Scheduler
    participant createOrUpdateDiscourseEmbedding
    participant Supabase
    participant FatalError

    rect rgb(230, 245, 245)
    Note over initializeSupabaseSync: Initialization Phase
    Caller->>initializeSupabaseSync: initializeSupabaseSync()
    initializeSupabaseSync->>initializeSupabaseSync: Check createClient() result
    alt Client valid
        initializeSupabaseSync->>Scheduler: Schedule first run via activeTimeout
        Scheduler-->>initializeSupabaseSync: activeTimeout set
    else Client invalid
        initializeSupabaseSync->>FatalError: FatalError raised
        FatalError-->>initializeSupabaseSync: Disable syncing
    end
    end

    rect rgb(245, 240, 230)
    Note over Scheduler: Scheduler Phase (Time-based Loop)
    loop Every aBASE_SYNC_INTERVAL ms
        Scheduler->>createOrUpdateDiscourseEmbedding: Execute sync
        alt doSync enabled & Client valid
            createOrUpdateDiscourseEmbedding->>Supabase: proposeSyncTask()
            Supabase-->>createOrUpdateDiscourseEmbedding: SyncTaskInfo {lastUpdateTime?, nextUpdateTime?}
            alt Task needs update
                createOrUpdateDiscourseEmbedding->>Supabase: Perform sync
                Supabase-->>createOrUpdateDiscourseEmbedding: Success
                createOrUpdateDiscourseEmbedding->>createOrUpdateDiscourseEmbedding: Reset numFailures=0
                createOrUpdateDiscourseEmbedding->>createOrUpdateDiscourseEmbedding: endSyncTask(showToast=true)
            else Task update failed
                createOrUpdateDiscourseEmbedding->>createOrUpdateDiscourseEmbedding: numFailures++
                alt numFailures < MAX_FAILURES
                    createOrUpdateDiscourseEmbedding->>createOrUpdateDiscourseEmbedding: Exponential backoff
                    createOrUpdateDiscourseEmbedding->>createOrUpdateDiscourseEmbedding: endSyncTask(showToast=true)
                else numFailures >= MAX_FAILURES
                    createOrUpdateDiscourseEmbedding->>FatalError: FatalError raised
                    FatalError-->>createOrUpdateDiscourseEmbedding: Stop scheduling
                end
            end
        else doSync disabled or setup error
            createOrUpdateDiscourseEmbedding->>createOrUpdateDiscourseEmbedding: Cancel activeTimeout
        end
    end
    end

    rect rgb(240, 245, 230)
    Note over Caller: Manual Control
    Caller->>Caller: setSyncActivity(false)
    Caller->>Scheduler: Cancel activeTimeout
    Caller->>Caller: setSyncActivity(true)
    Caller->>Scheduler: Resume scheduling
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Rationale: The changes concentrate significant logic restructuring into a single file, introducing persistent scheduler patterns with state management (activeTimeout, numFailures), exponential backoff strategy, FatalError handling for lifecycle control, and multiple interdependent exported functions. While file scope is narrow, the control-flow density, new scheduler semantics, and error handling pathways require careful review to verify correctness of state transitions, timeout cleanup, and failure recovery logic.

Possibly related PRs

Pre-merge checks

❌ 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 pull request title "ENG-298 repeat the sync at regular intervals" accurately reflects the main objective of the changeset. The raw summary clearly documents that the primary change is introducing a centralized sync control flow with a persistent, time-based scheduler using a BASE_SYNC_INTERVAL-based timeout mechanism to repeat syncing at regular intervals. The title is concise, specific, and directly conveys the core purpose of the changes without being vague or misleading. While the changeset includes additional improvements like error handling and type refactoring, the title appropriately captures the central theme that ties these changes together.

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

Caution

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

⚠️ Outside diff range comments (1)
apps/roam/src/utils/syncDgNodesToSupabase.ts (1)

327-329: Bug: Error constructor misused; second argument is ignored (should be options.cause).

Use cause to preserve the original error and include its message.

-throw new Error(`upsert_content failed for batch ${idx + 1}:`, error);
+throw new Error(
+  `upsert_content failed for batch ${idx + 1}: ${'message' in (error ?? {}) ? (error as any).message : String(error)}`,
+  { cause: error }
+);
🧹 Nitpick comments (3)
apps/roam/src/utils/syncDgNodesToSupabase.ts (3)

415-426: Schedule clamp: guard against negative or absurd delays.

Clock drift or rounding can yield negative/huge delays. Clamp to a sane minimum and optional max.

-        activeTimeout = setTimeout(
-          createOrUpdateDiscourseEmbedding, // eslint-disable-line @typescript-eslint/no-misused-promises
-          nextUpdateTime.valueOf() - Date.now() + 100,
-        );
+        const rawDelay = nextUpdateTime.valueOf() - Date.now() + 100;
+        const delay = Math.max(100, Math.min(rawDelay, 30 * 60 * 1000)); // [100ms, 30m]
+        activeTimeout = setTimeout(
+          createOrUpdateDiscourseEmbedding, // eslint-disable-line @typescript-eslint/no-misused-promises
+          delay,
+        );

475-485: Backoff lacks jitter and cap; add jitter to avoid thundering herd and cap extreme delays.

Introduce jitter and a max backoff. Also, confirm partial-success policy per team learning.

-  let timeout = BASE_SYNC_INTERVAL;
+  let timeout = BASE_SYNC_INTERVAL;
   if (success) {
     numFailures = 0;
   } else {
     numFailures += 1;
     if (numFailures >= MAX_FAILURES) {
       doSync = false;
       return;
     }
-    timeout *= 2 ** numFailures;
+    // Exponential backoff with jitter and cap
+    const maxBackoff = 30 * 60 * 1000; // 30 minutes
+    const exp = timeout * (2 ** numFailures);
+    const jitter = 0.9 + Math.random() * 0.2; // 0.9x–1.1x
+    timeout = Math.min(exp, maxBackoff) * jitter;
   }

Based on learnings: confirm that “partial successes should not trigger backoff.” Does any upstream call here return counts vs throwing? If counts can indicate partial progress, ensure success remains true and don’t enter backoff.


435-439: Minor: avoid creating and discarding a client instance.

createClient() is used as a truthy check but constructs a client you immediately drop. Prefer a dedicated “isConfigured” helper or reuse the instance.

-    if (!createClient()) {
+    const bootstrap = createClient();
+    if (!bootstrap) {
       // not worth retrying
       // TODO: Differentiate setup vs connetion error
       throw new FatalError("Could not access supabase.");
     }

Optionally plumb bootstrap through when you need a non-authenticated client.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b964097 and 6fb89d2.

📒 Files selected for processing (1)
  • apps/roam/src/utils/syncDgNodesToSupabase.ts (7 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-18T18:58:16.100Z
Learnt from: maparent
PR: DiscourseGraphs/discourse-graph#504
File: apps/roam/src/utils/syncDgNodesToSupabase.ts:523-531
Timestamp: 2025-10-18T18:58:16.100Z
Learning: In `apps/roam/src/utils/syncDgNodesToSupabase.ts`, partial successes from `upsertNodesToSupabaseAsContent` and `addMissingEmbeddings` (indicated by numeric return values showing the count of successful operations) should NOT trigger backoff. Only complete failures (false) should trigger the exponential backoff mechanism. This design allows the sync process to continue making progress even when some items fail.

Applied to files:

  • apps/roam/src/utils/syncDgNodesToSupabase.ts
🔇 Additional comments (3)
apps/roam/src/utils/syncDgNodesToSupabase.ts (3)

64-73: Toast gating looks good.

showToast is respected on both error paths; LGTM.

Also applies to: 91-97


392-405: Activation toggling is clean and race-safe enough for this scope.

Clears pending timeout on pause; schedules a quick run on resume. LGTM.


504-509: Init path: good first-run UX.

Conditional enable + initial toast run looks good.

@maparent maparent changed the title ENG-298 repeat the sync every 5 minutes ENG-298 repeat the sync at regular intervals Oct 24, 2025
@maparent maparent requested review from mdroidian and sid597 October 24, 2025 13:21
@sid597
Copy link
Collaborator

sid597 commented Oct 24, 2025

image

First not sure how I reached to this state but is it possible to have last_task_start > last_task_end? Maybe if the task failed or is in progress?

Copy link
Collaborator

@sid597 sid597 left a comment

Choose a reason for hiding this comment

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

LGTM

Currently we don't have a clear ui to stop sync for a user but we should keep this in mind for future when we add such a ui component, it should also stop syncActivity.

} from "./conceptConversion";
import { fetchEmbeddingsForNodes } from "./upsertNodesAsContentWithEmbeddings";
import { convertRoamNodeToLocalContent } from "./upsertNodesAsContentWithEmbeddings";
import { getRoamUrl } from "roamjs-components/dom";
Copy link
Collaborator

Choose a reason for hiding this comment

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

unused

doSync = false;
return;
}
const jitter = 0.9 + Math.random() * 0.2; // 0.9x–1.1x
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is this used for?

Copy link
Contributor

Choose a reason for hiding this comment

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

@maparent I am curious about this as well

Copy link
Collaborator Author

Thank you. Will merge just after #511 is merged.

@maparent maparent merged commit 8c61336 into main Oct 26, 2025
5 checks passed
@github-project-automation github-project-automation bot moved this to Done in General Oct 26, 2025
@maparent maparent deleted the eng-298-minimalist branch October 26, 2025 17:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants