Skip to content

Conversation

@maparent
Copy link
Collaborator

@maparent maparent commented Oct 18, 2025

Allows different steps to succeed independently;
Backoff time in case of errors.
Cancel on unload.
Opportunistic base-case fixing of ENG-975.

Summary by CodeRabbit

  • New Features

    • Added ability to enable/disable background syncing on demand.
    • Added optional toast notifications for sync operations.
  • Improvements

    • More reliable sync lifecycle (auto‑stop on unload and fatal conditions).
    • Sync operations now report success/failure more explicitly.
    • Orphaned-node cleanup now returns explicit success/failure for safer retries.
  • Bug Fixes

    • Prevents lingering sync loops after extension unload.

Allows different steps to succeed independently;
Backoff time in case of errors.
Cancel on unload.
@linear
Copy link

linear bot commented Oct 18, 2025

@supabase
Copy link

supabase bot commented Oct 18, 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 18, 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 18, 2025

📝 Walkthrough

Walkthrough

Adds a public sync control API (setSyncActivity) to the Roam extension, exposes it on window.roamjs.extension.queryBuilder, invokes it on unload to stop syncing, and refactors sync/cleanup utilities to return boolean success states and switch timestamp types from strings to Dates.

Changes

Cohort / File(s) Summary
Extension public surface
apps/roam/src/index.ts
Imports setSyncActivity from sync utilities, exposes it on window.roamjs.extension.queryBuilder, and calls setSyncActivity(false) during unload. Also changes a type-check directive for getDiscourseNodes.
Sync orchestration & helpers
apps/roam/src/utils/syncDgNodesToSupabase.ts
Introduces setSyncActivity(active: boolean) and internal doSync/backoff/error counters; updates endSyncTask to return Promise<boolean> and accept showToast; changes SyncTaskInfo timestamps to Date types and DEFAULT_TIME to new Date(...); adjusts function return types to `Promise<boolean
Orphan cleanup
apps/roam/src/utils/cleanupOrphanedNodes.ts
Changes helper fetch functions to return `Promise<string[]

Sequence Diagram(s)

sequenceDiagram
    participant Ext as Roam Extension
    participant API as queryBuilder (setSyncActivity)
    participant Sync as Sync Loop
    participant DB as Supabase

    Ext->>API: setSyncActivity(true)
    activate API
    API->>API: doSync = true, schedule activeTimeout
    API->>Sync: start/resume loop
    deactivate API

    loop while doSync
        Sync->>Sync: createOrUpdateDiscourseEmbedding(showToast?)
        alt Success
            Sync->>DB: upsert nodes/embeddings/users
            DB-->>Sync: OK
            Sync->>Sync: reset error counter
        else Transient error
            Sync->>Sync: increment error counter, backoff, maybe showToast
            Sync->>Sync: retry later
        else Fatal error
            Sync->>API: setSyncActivity(false)
            Note right of Sync: throw FatalError, stop loop
        end
    end

    Ext->>API: (on unload) setSyncActivity(false)
    activate API
    API->>API: doSync = false, clear timeouts
    API->>Sync: stop loop
    deactivate API
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Pre-merge checks

✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
Title Check ✅ Passed The title "Repeat calls to upsertNodesToSupabaseAsContent at regular intervals" is directly related to the changeset. The raw summary confirms that the PR implements a scheduled sync loop that calls various functions including upsertNodesToSupabaseAsContent at regular intervals, with backoff logic for errors and a new setSyncActivity API to control the loop. The title accurately captures this core behavioral change—scheduling repetitive sync calls—which is a real and significant part of the changeset. While the PR also includes internal refactoring (return type changes from void to boolean) and additional features (error handling, cancellation support), the title focuses on one primary feature without needing to cover all implementation details, which is appropriate for a PR title. There is a minor typo in the title ("itervals" instead of "intervals"), but this doesn't affect the clarity or relatedness of the title to the changeset.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

🧹 Nitpick comments (4)
apps/roam/src/utils/cleanupOrphanedNodes.ts (1)

362-398: Return value doesn’t reflect inner failures; consider propagating step outcomes

Helpers log errors and return void/arrays, so this function returns true even if deletions fail. If the caller uses this boolean to drive backoff, it will under-report failures.

Recommend: have deleteNodesFromSupabase and get* helpers return booleans (or counts) and aggregate them here (return false on any failure). Optionally align with the pattern used elsewhere (boolean | number for partial success). I can draft a follow-up patch if you want.

apps/roam/src/index.ts (1)

145-146: Expose setSyncActivity on the public API

Good addition; enables pausing/resuming sync externally. Consider documenting this in your extension’s public API notes to avoid accidental misuse.

apps/roam/src/utils/syncDgNodesToSupabase.ts (2)

518-523: Release the sync task when the client/context is unavailable

If supabaseClient is null, you return early without calling endSyncTask, potentially holding the task until timeout. End the task as failed before returning.

-    const supabaseClient = await getLoggedInClient();
-    if (!supabaseClient) return;
+    const supabaseClient = await getLoggedInClient();
+    if (!supabaseClient) {
+      await endSyncTask(worker, "failed", showToast);
+      return;
+    }
     const context = await getSupabaseContext();
     if (!context) {
       await endSyncTask(worker, "failed");
       // not worth retrying
       throw new FatalError("No Supabase context found.");
     }

501-507: Propagate showToast to scheduled retries/postponements (optional)

Scheduled calls drop the showToast arg, changing UX after the first run. If you want consistent behavior, pass it through:

-        activeTimeout = setTimeout(
-          createOrUpdateDiscourseEmbedding,
-          nextUpdateTime.valueOf() - Date.now() + 100,
-        );
+        activeTimeout = setTimeout(
+          () => void createOrUpdateDiscourseEmbedding(showToast),
+          nextUpdateTime.valueOf() - Date.now() + 100,
+        );
-    activeTimeout = setTimeout(createOrUpdateDiscourseEmbedding, timeout);
+    activeTimeout = setTimeout(() => void createOrUpdateDiscourseEmbedding(showToast), timeout);
-    activeTimeout = setTimeout(createOrUpdateDiscourseEmbedding, 100, true);
+    activeTimeout = setTimeout(() => void createOrUpdateDiscourseEmbedding(true), 100);

Also applies to: 565-566, 582-583

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 31aa1f1 and 506e0c1.

📒 Files selected for processing (3)
  • apps/roam/src/index.ts (3 hunks)
  • apps/roam/src/utils/cleanupOrphanedNodes.ts (2 hunks)
  • apps/roam/src/utils/syncDgNodesToSupabase.ts (10 hunks)
🔇 Additional comments (2)
apps/roam/src/index.ts (2)

30-33: Import and public surface LGTY

Importing initializeSupabaseSync and setSyncActivity here is appropriate given the new sync loop. No issues.


159-159: Stop the sync loop on unload

Calling setSyncActivity(false) on unload is correct and prevents stray timers. Nice.

@maparent
Copy link
Collaborator Author

@CodeRabbit review latest changes

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 18, 2025

@maparent: I'll review the latest changes in this PR.

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

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

Caution

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

⚠️ Outside diff range comments (2)
apps/roam/src/utils/cleanupOrphanedNodes.ts (2)

4-85: Inconsistent error handling masks failures.

Lines 21 and 83 correctly return null on error, but lines 47 and 70 return empty arrays on error. The caller at line 374 checks for null to detect failures, so errors at lines 47 and 70 will be silently treated as "no results" rather than triggering the failure path.

Apply this diff to return null consistently:

     if (conceptResponse.error) {
       console.error(
         "Failed to get concepts from Supabase:",
         conceptResponse.error,
       );
-      return [];
+      return null;
     }
     if (blockContentResponse.error) {
       console.error(
         "Failed to get block content from Supabase:",
         blockContentResponse.error,
       );
-      return [];
+      return null;
     }

364-407: The review comment is valid but could be more precise about the root cause.

The identified inconsistency is genuine: deleteNodesFromSupabase returns a boolean, allowing cleanupOrphanedNodes to track failures via success &&= (line 380), while deleteNodeSchemasFromSupabase returns a numeric count. Critically, this function cannot distinguish between "0 items deleted (success)" and "error occurred" (both return 0), and more importantly, the caller ignores the return value entirely at line 396-400. This means schema cleanup failures silently bypass success tracking, while node cleanup failures correctly propagate via the success variable.

The function returns true even if orphaned schema deletion fails, which is inconsistent with how node deletion failures are handled.

Suggested fix: Either track the schema deletion result (success &&= (deleted > 0 && orphanedSchemaUids.length > 0) or similar), or document that schema cleanup failures are intentionally non-blocking.

🧹 Nitpick comments (1)
apps/roam/src/utils/syncDgNodesToSupabase.ts (1)

495-507: Consider improving error message clarity.

Line 497 throws a generic error when nextUpdateTime is undefined. This can occur when proposeSyncTask encounters transient issues (e.g., network errors). While the catch block will handle it, a more descriptive message would aid debugging.

       if (nextUpdateTime === undefined) {
-        throw new Error("Can't obtain sync task");
+        throw new Error("proposeSyncTask failed to return a valid timestamp");
       }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 506e0c1 and bbdcf69.

📒 Files selected for processing (3)
  • apps/roam/src/index.ts (3 hunks)
  • apps/roam/src/utils/cleanupOrphanedNodes.ts (11 hunks)
  • apps/roam/src/utils/syncDgNodesToSupabase.ts (10 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-18T18:58:16.089Z
Learnt from: maparent
PR: DiscourseGraphs/discourse-graph#504
File: apps/roam/src/utils/syncDgNodesToSupabase.ts:523-531
Timestamp: 2025-10-18T18:58:16.089Z
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 (19)
apps/roam/src/index.ts (4)

30-33: LGTM – imports are correct.

The new imports for initializeSupabaseSync and setSyncActivity are properly sourced and used later in the file.


143-144: Good practice – prefer ts-expect-error over ts-ignore.

This change ensures the suppression fails if the type error is ever resolved, preventing stale directives.


145-145: LGTM – public API exposure is appropriate.

Exposing setSyncActivity on the extension's queryBuilder API allows external control of the sync process, which aligns with the PR's goal of providing sync control.


159-161: Proper cleanup on unload.

Calling setSyncActivity(false) ensures the sync loop is stopped and any pending timeout is cleared when the extension unloads. This prevents resource leaks and orphaned timers.

apps/roam/src/utils/cleanupOrphanedNodes.ts (2)

87-126: LGTM – consistent null returns on error.

The function properly returns null on all error paths, making failures easy to detect.


148-197: LGTM – proper boolean success/failure reporting.

All error paths return false, and the success path returns true. The boolean return allows the caller to track success state.

apps/roam/src/utils/syncDgNodesToSupabase.ts (13)

23-24: LGTM – import updates are correct.

The imports properly source types and client utilities from the database package.


33-33: LGTM – DEFAULT_TIME now a Date object.

This change aligns with the updated timestamp types (lastUpdateTime and nextUpdateTime are now Date objects) and simplifies usage at line 509.


35-41: LGTM – optional Date types are appropriate.

Making lastUpdateTime and nextUpdateTime optional reflects their conditional presence in different response scenarios.


43-101: Well-implemented boolean return and optional toasts.

The function now properly returns boolean on all paths (including the early exits at lines 50 and 54), and the showToast parameter provides flexible feedback control. The implementation addresses previous concerns about the return type contract.


103-175: LGTM – proper timestamp handling with Date objects.

The function correctly distinguishes between "wait until" (future nextUpdateTime) and "proceed with" (past lastUpdateTime) scenarios.


318-382: LGTM – proper partial success semantics.

The return values correctly distinguish complete success (true), partial success (count as number), and complete failure (false), which aligns with the design goal of avoiding backoff on partial progress. Based on learnings.


384-413: LGTM – consistent partial success handling.

The function follows the same pattern as addMissingEmbeddings, properly distinguishing complete, partial, and failed outcomes. Based on learnings.


445-458: LGTM – boolean success indicator.

The function properly returns true when the error is null and false otherwise.


460-468: LGTM – proper sync control infrastructure.

The constants, state variables, and FatalError class provide a solid foundation for the scheduled sync with backoff mechanism.


470-482: LGTM – proper sync toggle implementation.

The function correctly manages the doSync flag and activeTimeout. The guard at line 475 prevents spawning duplicate timers, and the 100ms initial delay provides a quick start when activating.


508-546: LGTM – proper error handling and partial success semantics.

Line 509 correctly uses toISOString() for precise timestamp formatting. The distinction between FatalError (unrecoverable) and regular Error (retryable) is well-designed. Lines 535 and 537 properly treat numeric results (partial success) as non-failures, aligning with the design goal of continuing progress even when some items fail. Based on learnings.


547-575: LGTM – robust error handling with exponential backoff.

The catch block distinguishes fatal from retryable errors. The exponential backoff (multiplier: 2^numFailures) provides reasonable intervals (10m, 20m, 40m, 80m before stopping), and the checks at lines 567-571 prevent orphaned timers.


577-592: LGTM – conditional initialization based on space existence.

The initialization properly checks for space registration before enabling sync. The initial 100ms delay and showToast: true provide immediate user feedback on first run.

);
}
if (supabaseSchemaUids === null) return false;
if (supabaseSchemaUids.length === 0) return true;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Early return masks previous failures.

Line 393 returns true unconditionally when there are no schemas to clean up, but this occurs after node cleanup operations that may have failed. If success is already false from lines 374-386, this early return will incorrectly report success.

Apply this diff to respect prior failures:

-    if (supabaseSchemaUids.length === 0) return true;
+    if (supabaseSchemaUids.length === 0) return success;
📝 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
if (supabaseSchemaUids.length === 0) return true;
if (supabaseSchemaUids.length === 0) return success;
🤖 Prompt for AI Agents
In apps/roam/src/utils/cleanupOrphanedNodes.ts around line 393, the function
currently does "if (supabaseSchemaUids.length === 0) return true;" which masks
any prior failures recorded in the `success` flag; change this early return to
return the existing `success` variable instead (or compute and return
`success`), so that if earlier cleanup steps set `success = false` those
failures are propagated; ensure `success` is in scope and returned when there
are no schemas to process.

@maparent maparent marked this pull request as ready for review October 18, 2025 19:32
@maparent maparent requested a review from mdroidian October 18, 2025 19:33
@maparent maparent changed the title ENG-298 Repeat calls upsertNodesToSupabaseAsContent at regular itervals. ENG-298 Repeat calls to upsertNodesToSupabaseAsContent at regular itervals. Oct 18, 2025
@maparent maparent marked this pull request as draft October 19, 2025 14:23
@maparent maparent removed the request for review from mdroidian October 19, 2025 14:23
@maparent
Copy link
Collaborator Author

Ok, decided to count partial success as failure for now; I wrote about the possibility of recording a partial success eventually, but I think that is an optimization, less urgent, and scope creep.

@maparent
Copy link
Collaborator Author

@CodeRabbit review last commit.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 19, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the last commit on the PR branch
echo "=== Last commit details ==="
git log -1 --pretty=format:"Commit: %H%nAuthor: %an <%ae>%nDate: %ad%nMessage: %s%n%b" --date=iso

echo -e "\n\n=== Files changed in last commit ==="
git diff-tree --no-commit-id --name-status -r HEAD

echo -e "\n\n=== Full diff of last commit ==="
git show HEAD --format=fuller

Length of output: 7423


@maparent: I've reviewed the last commit. I found a critical logic error in how partial success is being evaluated, along with some good refactoring decisions.

🚨 Critical Issue

File: apps/roam/src/utils/syncDgNodesToSupabase.ts
Line: ~522

The logic for counting partial success as failure is inverted:

success &&= partial !== true;

This currently does:

  • partial === true (full success) → success &&= falsemarks success as false
  • partial === <number> (partial success) → success &&= truekeeps success unchanged
  • partial === false (complete failure) → success &&= truekeeps success unchanged

Should be:

success &&= partial === true;

This would correctly:

  • partial === true (full success) → success &&= true → keeps success true ✓
  • partial === <number> (partial success) → success &&= false → marks as failure ✓
  • partial === false (complete failure) → success &&= false → marks as failure ✓

✅ Good Changes

  1. Return type additions: Adding Promise<boolean> to upsertNodeSchemaToContent and convertDgToSupabaseConcepts improves type safety and error handling.

  2. Signature simplification: Refactoring convertRoamNodeToLocalContent to take nodes directly instead of wrapped in an object is cleaner.

  3. Non-time-dependent operations: Moving addMissingEmbeddings and cleanupOrphanedNodes outside the success path makes sense per your comment—they can run independently without affecting the sync status.

  4. Conditional concept conversion: The logic to pass only successfully uploaded nodes to convertDgToSupabaseConcepts (lines 546-551) is well thought out.

  5. TODO comments: The comments about potential optimizations are helpful for future work.

Recommendation

Fix the logic error on line ~522 before merging.

@maparent
Copy link
Collaborator Author

@CodeRabbit good catch on the logic error.

@maparent maparent marked this pull request as ready for review October 19, 2025 15:10
@maparent maparent requested a review from mdroidian October 19, 2025 15:10
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 19, 2025

Tip

For best results, initiate chat on the files or code changes.

@maparent: You're welcome! Glad I could help spot that before it became a harder-to-debug issue. The inverted logic would have caused the opposite behavior of what you intended.

Once you fix line ~522 to success &&= partial === true;, the partial-success-as-failure semantics will work correctly. Good luck with the rest of the PR! 🚀


🧠 Learnings used
Learnt from: maparent
PR: DiscourseGraphs/discourse-graph#504
File: apps/roam/src/utils/syncDgNodesToSupabase.ts:523-531
Timestamp: 2025-10-18T18:58:16.089Z
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.

@mdroidian mdroidian requested a review from sid597 October 20, 2025 01:05
@mdroidian
Copy link
Contributor

@sid597 could you review this one as well

@maparent maparent marked this pull request as draft October 23, 2025 18:26
@maparent
Copy link
Collaborator Author

leaving this as a draft, as it contains the dependency sequence refactor, which I think is still useful.

@maparent maparent closed this Nov 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

No open projects
Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants