Skip to content

fix: structuredClone fallback for older Android WebView#1018

Merged
chhoumann merged 1 commit intomasterfrom
fix/android-structuredclone-fallback
Dec 15, 2025
Merged

fix: structuredClone fallback for older Android WebView#1018
chhoumann merged 1 commit intomasterfrom
fix/android-structuredclone-fallback

Conversation

@chhoumann
Copy link
Copy Markdown
Owner

@chhoumann chhoumann commented Dec 15, 2025

Fixes Android load failures where structuredClone is missing/throws (seen in #786 and #777).

  • Add deepClone() helper that uses structuredClone when available, with a safe fallback
  • Replace direct structuredClone(...) calls in runtime code paths (settings init, migrations, package import/export, etc.)
  • Add regression tests for the fallback clone

Fixes #786

Summary by CodeRabbit

  • Refactor

    • Introduced a robust deep cloning utility to improve reliability of object duplication across the application, with enhanced handling for edge cases, circular references, and class instances.
  • Tests

    • Added comprehensive test coverage for deep cloning operations, including fallback behavior and circular reference handling.

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

@vercel
Copy link
Copy Markdown

vercel Bot commented Dec 15, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
quickadd Ready Ready Preview Dec 15, 2025 8:44pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 15, 2025

Walkthrough

A new deepClone utility function is introduced with fallback support and systematically replaces all structuredClone usages throughout the codebase with imports and calls to this custom deep-cloning utility.

Changes

Cohort / File(s) Summary
New utility function and tests
src/utils/deepClone.ts, src/utils/deepClone.test.ts
Adds deepClone<T>() export with structuredClone-first implementation and custom cycle-safe fallback; includes comprehensive unit tests covering primitives, Date, Array, Map, Set, RegExp, ArrayBuffer, TypedArray views, class instances, and circular references.
GUI components
src/gui/AIAssistantProvidersModal.ts, src/gui/MacroGUIs/ConditionalBranchEditorModal.ts
Replace structuredClone with deepClone import and usage; AIAssistantProvidersModal also moves reload() invocation into Edit button onClick handler.
Migration utilities
src/migrations/incrementFileNameSettingMoveToDefaultBehavior.ts, src/migrations/migrate.ts, src/migrations/mutualExclusionInsertAfterAndWriteToBottomOfFile.ts, src/migrations/setProviderModelDiscoveryMode.ts
Import deepClone and replace structuredClone calls in settings and provider cloning operations.
Service layer
src/services/choiceService.ts, src/services/packageExportService.ts, src/services/packageImportService.ts
Import deepClone and replace structuredClone calls when cloning choice objects, macro fields, and package state.
Store and utility modules
src/settingsStore.ts, src/utilityObsidian.ts
Import deepClone and replace structuredClone in default settings initialization and excludeKeys utility function.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Key areas requiring attention:
    • src/utils/deepClone.ts: Review the fallback implementation for correctness, particularly cycle detection logic, TypedArray view handling, and prototype preservation
    • src/utils/deepClone.test.ts: Verify test coverage is sufficient for all code paths, especially fallback scenarios and edge cases
    • Systematic replacement pattern: Spot-check a few of the replacement sites to ensure deepClone calls match structuredClone's original context and arity

Poem

🐰 Deep down we clone, through tunnels of thought,
When structured systems fail, a fallback is sought.
Circles and references, now safely preserved,
Our custom deepClone utility, perfectly served.
No shallow graves here—just copies profound! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% 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 change: introducing a fallback mechanism for structuredClone to handle older Android WebView environments where it is unavailable or throws errors.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/android-structuredclone-fallback

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

@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/migrations/incrementFileNameSettingMoveToDefaultBehavior.ts (1)

4-4: Migration cloning strategy preserved with deepClone

Using deepClone for plugin.settings.choices and macros before running the migration helpers keeps the “don’t mutate original settings in place” behavior, and the final plugin.settings.choices = deepClone(choices) write remains safe, just slightly redundant. No new migration‑logic risks introduced.

Also applies to: 52-52, 55-55, 58-58

src/utils/deepClone.ts (1)

1-118: Robust deepClone implementation; note object‑descriptor behavior

The structuredClone‑first strategy with a cycle‑aware fallback covers the important built‑ins (Date, Array, Map/Set, RegExp, ArrayBuffer/views) and should be more than sufficient for the plugin’s settings and choice trees.

One behavioral difference vs native structuredClone is in the generic object branch: you currently clone only enumerable own string keys and always emit simple data properties, so non‑enumerable properties, symbol keys, and getters/setters are flattened or dropped. That’s fine for today’s usage (plain data), but if you ever feed richer class instances through this helper, this can subtly change behavior.

If you want to more closely preserve object shapes while still using the same fallback, you can switch to cloning full property descriptors; for example:

-	const prototype = Object.getPrototypeOf(value);
-	const cloned: Record<string, unknown> = Object.create(prototype);
-	seen.set(value, cloned);
-	for (const [key, nestedValue] of Object.entries(value)) {
-		Object.defineProperty(cloned, key, {
-			value: deepCloneFallback(nestedValue, seen),
-			writable: true,
-			enumerable: true,
-			configurable: true,
-		});
-	}
-	return cloned;
+	const prototype = Object.getPrototypeOf(value);
+	const cloned: Record<string, unknown> = Object.create(prototype);
+	seen.set(value, cloned);
+
+	const descriptors = Object.getOwnPropertyDescriptors(value as object);
+	for (const [key, descriptor] of Object.entries(descriptors)) {
+		const clonedDescriptor = { ...descriptor };
+		if ("value" in clonedDescriptor) {
+			clonedDescriptor.value = deepCloneFallback(
+				clonedDescriptor.value,
+				seen,
+			);
+		}
+		Object.defineProperty(cloned, key, clonedDescriptor);
+	}
+	return cloned;

Not required for this PR, but it would make the fallback even closer to native structuredClone semantics if you start cloning more complex instances.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cf38d88 and 41411a8.

📒 Files selected for processing (13)
  • src/gui/AIAssistantProvidersModal.ts (2 hunks)
  • src/gui/MacroGUIs/ConditionalBranchEditorModal.ts (2 hunks)
  • src/migrations/incrementFileNameSettingMoveToDefaultBehavior.ts (2 hunks)
  • src/migrations/migrate.ts (2 hunks)
  • src/migrations/mutualExclusionInsertAfterAndWriteToBottomOfFile.ts (2 hunks)
  • src/migrations/setProviderModelDiscoveryMode.ts (2 hunks)
  • src/services/choiceService.ts (2 hunks)
  • src/services/packageExportService.ts (2 hunks)
  • src/services/packageImportService.ts (3 hunks)
  • src/settingsStore.ts (1 hunks)
  • src/utilityObsidian.ts (2 hunks)
  • src/utils/deepClone.test.ts (1 hunks)
  • src/utils/deepClone.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Source code lives in src/: core logic under engine/, services/, and utils/; Svelte UI in src/gui; shared types in src/types; settings entry in src/quickAddSettingsTab.ts

Files:

  • src/gui/MacroGUIs/ConditionalBranchEditorModal.ts
  • src/migrations/incrementFileNameSettingMoveToDefaultBehavior.ts
  • src/utils/deepClone.test.ts
  • src/migrations/setProviderModelDiscoveryMode.ts
  • src/services/choiceService.ts
  • src/migrations/migrate.ts
  • src/utils/deepClone.ts
  • src/services/packageImportService.ts
  • src/settingsStore.ts
  • src/utilityObsidian.ts
  • src/services/packageExportService.ts
  • src/gui/AIAssistantProvidersModal.ts
  • src/migrations/mutualExclusionInsertAfterAndWriteToBottomOfFile.ts
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.{ts,tsx}: Biome enforces tab indentation (width 2), LF endings, and an 80-character line guide; align editor settings
Use camelCase for variables and functions
Prefer type-only imports in TypeScript files
Route logging through the logger utilities for consistent output
Structure production code so Obsidian dependencies are injected behind interfaces; unit tests target pure logic and swap in adapters or tests/obsidian-stub.ts

Files:

  • src/gui/MacroGUIs/ConditionalBranchEditorModal.ts
  • src/migrations/incrementFileNameSettingMoveToDefaultBehavior.ts
  • src/utils/deepClone.test.ts
  • src/migrations/setProviderModelDiscoveryMode.ts
  • src/services/choiceService.ts
  • src/migrations/migrate.ts
  • src/utils/deepClone.ts
  • src/services/packageImportService.ts
  • src/settingsStore.ts
  • src/utilityObsidian.ts
  • src/services/packageExportService.ts
  • src/gui/AIAssistantProvidersModal.ts
  • src/migrations/mutualExclusionInsertAfterAndWriteToBottomOfFile.ts
src/**/*.{ts,tsx,svelte}

📄 CodeRabbit inference engine (AGENTS.md)

Use PascalCase for classes and Svelte components

Files:

  • src/gui/MacroGUIs/ConditionalBranchEditorModal.ts
  • src/migrations/incrementFileNameSettingMoveToDefaultBehavior.ts
  • src/utils/deepClone.test.ts
  • src/migrations/setProviderModelDiscoveryMode.ts
  • src/services/choiceService.ts
  • src/migrations/migrate.ts
  • src/utils/deepClone.ts
  • src/services/packageImportService.ts
  • src/settingsStore.ts
  • src/utilityObsidian.ts
  • src/services/packageExportService.ts
  • src/gui/AIAssistantProvidersModal.ts
  • src/migrations/mutualExclusionInsertAfterAndWriteToBottomOfFile.ts
🧠 Learnings (2)
📚 Learning: 2025-12-09T21:20:52.398Z
Learnt from: CR
Repo: chhoumann/quickadd PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-09T21:20:52.398Z
Learning: Applies to tests/**/*.{ts,tsx} : Add regression coverage for bug fixes

Applied to files:

  • src/utils/deepClone.test.ts
📚 Learning: 2025-12-09T21:20:52.398Z
Learnt from: CR
Repo: chhoumann/quickadd PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-09T21:20:52.398Z
Learning: Applies to src/**/*.{ts,tsx} : Structure production code so Obsidian dependencies are injected behind interfaces; unit tests target pure logic and swap in adapters or `tests/obsidian-stub.ts`

Applied to files:

  • src/utils/deepClone.test.ts
  • src/utilityObsidian.ts
🧬 Code graph analysis (12)
src/gui/MacroGUIs/ConditionalBranchEditorModal.ts (1)
src/utils/deepClone.ts (1)
  • deepClone (1-15)
src/migrations/incrementFileNameSettingMoveToDefaultBehavior.ts (1)
src/utils/deepClone.ts (1)
  • deepClone (1-15)
src/utils/deepClone.test.ts (1)
src/utils/deepClone.ts (1)
  • deepClone (1-15)
src/migrations/setProviderModelDiscoveryMode.ts (1)
src/utils/deepClone.ts (1)
  • deepClone (1-15)
src/services/choiceService.ts (2)
src/types/choices/IMacroChoice.ts (1)
  • IMacroChoice (4-7)
src/utils/deepClone.ts (1)
  • deepClone (1-15)
src/migrations/migrate.ts (1)
src/utils/deepClone.ts (1)
  • deepClone (1-15)
src/services/packageImportService.ts (1)
src/utils/deepClone.ts (1)
  • deepClone (1-15)
src/settingsStore.ts (2)
src/quickAddSettingsTab.ts (2)
  • QuickAddSettings (14-54)
  • DEFAULT_SETTINGS (56-91)
src/utils/deepClone.ts (1)
  • deepClone (1-15)
src/utilityObsidian.ts (1)
src/utils/deepClone.ts (1)
  • deepClone (1-15)
src/services/packageExportService.ts (1)
src/utils/deepClone.ts (1)
  • deepClone (1-15)
src/gui/AIAssistantProvidersModal.ts (1)
src/utils/deepClone.ts (1)
  • deepClone (1-15)
src/migrations/mutualExclusionInsertAfterAndWriteToBottomOfFile.ts (1)
src/utils/deepClone.ts (1)
  • deepClone (1-15)
🪛 ast-grep (0.40.0)
src/utils/deepClone.ts

[warning] 62-62: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(value.source, value.flags)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🔇 Additional comments (11)
src/gui/MacroGUIs/ConditionalBranchEditorModal.ts (1)

6-6: LGTM! Centralized cloning utility integrated correctly.

The migration from a local cloneCommands function to the project-wide deepClone utility is appropriate and ensures consistent cloning behavior with Android WebView fallback support.

Also applies to: 36-36

src/migrations/migrate.ts (1)

12-12: LGTM! Critical migration backup now has Android WebView support.

Replacing structuredClone with deepClone for the migration backup ensures that settings backups work on older Android WebView environments where structuredClone may be unavailable.

Also applies to: 42-42

src/services/packageExportService.ts (1)

19-19: LGTM! Package export now supports older Android WebView.

The switch from structuredClone to deepClone when cloning choices for package export ensures compatibility with environments where structuredClone is unavailable.

Also applies to: 68-68

src/migrations/setProviderModelDiscoveryMode.ts (1)

4-4: LGTM! Provider migration now supports older Android WebView.

Replacing structuredClone with deepClone ensures this migration can clone provider configurations on older Android WebView environments.

Also applies to: 29-29

src/services/choiceService.ts (1)

21-21: LGTM! Choice duplication now supports older Android WebView.

The replacement of structuredClone with deepClone when duplicating macro choices ensures compatibility with older Android WebView environments.

Also applies to: 53-53

src/utilityObsidian.ts (1)

24-24: LGTM! Utility function now supports older Android WebView.

Replacing structuredClone with deepClone in the excludeKeys utility function ensures it works on older Android WebView environments where structuredClone may be unavailable.

Also applies to: 949-949

src/settingsStore.ts (1)

4-4: LGTM! Critical settings initialization now supports older Android WebView.

This change is essential for the PR's goal. The settings store initialization with deepClone(DEFAULT_SETTINGS) ensures the plugin can load on older Android WebView environments where structuredClone is unavailable (addressing issues #786 and #777).

Also applies to: 10-10

src/migrations/mutualExclusionInsertAfterAndWriteToBottomOfFile.ts (1)

7-7: LGTM! Migration cloning now supports older Android WebView.

Both cloning operations (choices and macros) are correctly updated from structuredClone to deepClone, ensuring this migration works on older Android WebView environments.

Also applies to: 52-52, 55-55

src/gui/AIAssistantProvidersModal.ts (1)

9-9: Provider edit cloning and reload wiring look sound

Importing deepClone and using it to snapshot the provider before entering edit mode keeps Cancel/Save semantics intact while avoiding structuredClone on older environments. The reload() call inside the Edit handler correctly re-renders only after selectedProvider and its clone are set, without introducing new state bugs.

Also applies to: 105-112

src/services/packageImportService.ts (1)

23-23: deepClone integration for package import looks correct

Switching from structuredClone to deepClone for existingChoices and each entry.choice preserves the previous “work on copies” behavior while adding the Android‑safe fallback. The subsequent ID remapping and tree mutation still operate only on clones, so neither options.existingChoices nor pkg.choices are mutated unexpectedly.

Also applies to: 263-263, 276-279

src/utils/deepClone.test.ts (1)

1-67: Deep clone tests give solid fallback coverage

The suite exercises the key behaviors: missing structuredClone, circular references, throwing structuredClone, and class instances, while restoring the original global after each test. This provides good regression protection for the new utility and its Android‑driven fallback path. Based on learnings, this aligns well with the “add regression coverage for bug fixes” guideline.

@chhoumann chhoumann merged commit d1a3eed into master Dec 15, 2025
4 checks passed
@chhoumann chhoumann deleted the fix/android-structuredclone-fallback branch December 15, 2025 21:10
github-actions Bot pushed a commit that referenced this pull request Dec 15, 2025
## [2.9.3](2.9.2...2.9.3) (2025-12-15)

### Bug Fixes

* add structuredClone fallback for Android ([#1018](#1018)) ([d1a3eed](d1a3eed))
@github-actions
Copy link
Copy Markdown

🎉 This PR is included in version 2.9.3 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@github-actions
Copy link
Copy Markdown

🚀 Release has been published: v2.9.3

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] The plugin failed to open in Android

1 participant