Skip to content

Commit 2288a9d

Browse files
fix: resolve settings revert, infinite reactive loops, and double-unmount in 4.9.0 (#685)
- Convert plugin.settings to getter/setter so TS mutations route through the Svelte 5 proxy, fixing view settings reverting on reload - Wrap proxy writes in untrack() in Matrix, TreeView, and TrailView effects to break effect_update_depth_exceeded infinite loops - Wrap $effect.pre proxy reads in untrack() so they only re-run on plugin prop changes, not on every settings write-back - Debounce REDRAW_SIDE_VIEWS handler (100ms) in Matrix and Tree views to prevent double-unmount and redundant remounts during load - Serialize codeblock fatal errors as readable plain-text log lines - Bump version to 4.9.1 and document fixes in CHANGELOG
1 parent 1b368b1 commit 2288a9d

12 files changed

Lines changed: 58 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file. See [standa
44

55
## 4.X
66

7+
### [4.9.1](https://github.com/SkepticMystic/breadcrumbs/compare/4.9.0...4.9.1) (2026-05-11)
8+
9+
### Bug Fixes
10+
11+
* Fix view settings (trail enabled, prev/next enabled, matrix collapse) reverting on reload — settings written by TypeScript callbacks were lost because the Svelte 5 reactive proxy did not propagate plain-object mutations back to its internal signal sources. Converted `plugin.settings` to a getter/setter so all writes route through the proxy.
12+
* Fix infinite reactive loop (`effect_update_depth_exceeded`) triggered when toggling trail or side view settings — `$effect` bodies in Matrix, TreeView, and TrailView were writing back to `plugin.settings` without `untrack()`, causing the proxy signal sources to re-trigger the same effects endlessly. Wrapped all proxy writes in `untrack()`.
13+
* Fix `$effect.pre` in Matrix, TreeView, and TrailView re-running on every proxy write — the effects tracked proxy signal sources when reading `plugin.settings.views.*` to initialise local settings state, creating a feedback loop with the write-back `$effect`. Wrapped the snapshot call in `untrack()` so `$effect.pre` only re-runs when the `plugin` prop itself changes.
14+
* Fix `lifecycle_double_unmount` error — concurrent `REDRAW_SIDE_VIEWS` events caused multiple `onOpen()` calls to race; both saw the same live component and both tried to unmount it. Debounced the event-driven `onOpen()` calls (100 ms trailing) so burst events collapse to a single remount.
15+
* Fix Matrix and Tree side views remounting on every graph update or active-leaf change — debounced `REDRAW_SIDE_VIEWS` handler collapses rapid-fire workspace events into one remount per burst.
16+
* Fix codeblock fatal-error log entries showing unexpandable `{…}` objects in the Obsidian developer console — errors are now logged as a formatted plain-text string with one line per error.
17+
718
### [4.8.2](https://github.com/SkepticMystic/breadcrumbs/compare/4.8.1...4.8.2) (2026-05-03)
819

920
### Bug Fixes

manifest-beta.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"id": "breadcrumbs",
33
"name": "Breadcrumbs",
4-
"version": "4.9.0",
4+
"version": "4.9.1",
55
"minAppVersion": "1.12.0",
66
"description": "Add structured hierarchies to your notes",
77
"author": "SkepticMystic",

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"id": "breadcrumbs",
33
"name": "Breadcrumbs",
4-
"version": "4.9.0",
4+
"version": "4.9.1",
55
"minAppVersion": "1.12.0",
66
"description": "Add structured hierarchies to your notes",
77
"author": "SkepticMystic",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "breadcrumbs",
3-
"version": "4.9.0",
3+
"version": "4.9.1",
44
"description": "Add typed-links to your Obsidian notes",
55
"main": "main.js",
66
"scripts": {

src/codeblocks/MDRC.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@ export class CodeblockMDRC extends MarkdownRenderChild {
6868
log.debug(timer_inner.elapsedMessage("Codeblocks.parse_source", true));
6969

7070
if (!parsed) {
71-
log.warn("fatal codeblock errors", errors);
71+
log.warn(
72+
"fatal codeblock errors\n" +
73+
errors.map((e) => ` [${e.code}] ${e.path}: ${e.message}`).join("\n"),
74+
);
7275

7376
mount(CodeblockErrors, {
7477
target: this.containerEl,

src/components/page_views/TrailView.svelte

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
if (last_plugin !== plugin) {
3333
last_plugin = plugin;
3434
settings = json_clone(
35-
$state.snapshot(plugin.settings.views.page.trail),
35+
untrack(() => $state.snapshot(plugin.settings.views.page.trail)),
3636
);
3737
}
3838
});
@@ -44,9 +44,10 @@
4444
let is_initial_mount = true;
4545
4646
$effect(() => {
47-
// Keep `plugin.settings.views.page.trail` aligned with the local `settings`
48-
// clone (same pattern as Matrix). Skip persisting on the first run only.
49-
plugin.settings.views.page.trail = $state.snapshot(settings);
47+
const trail_snapshot = $state.snapshot(settings);
48+
untrack(() => {
49+
plugin.settings.views.page.trail = trail_snapshot;
50+
});
5051
if (is_initial_mount) {
5152
is_initial_mount = false;
5253
} else {

src/components/side_views/Matrix.svelte

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,19 @@
3434
if (last_plugin !== plugin) {
3535
last_plugin = plugin;
3636
settings = json_clone(
37-
$state.snapshot(plugin.settings.views.side.matrix),
37+
untrack(() => $state.snapshot(plugin.settings.views.side.matrix)),
3838
);
3939
}
4040
});
4141
4242
let is_initial_mount = true;
4343
4444
$effect(() => {
45-
plugin.settings.views.side.matrix = $state.snapshot(settings);
46-
untrack(() => void plugin.saveSettings());
47-
// We only want to run this when *we* have changed `settings`,
48-
// and not when the component is initially mounted into the DOM,
49-
// or when the settings have been updated externally.
45+
const matrix_snapshot = $state.snapshot(settings);
46+
untrack(() => {
47+
plugin.settings.views.side.matrix = matrix_snapshot;
48+
void plugin.saveSettings();
49+
});
5050
if (is_initial_mount) {
5151
is_initial_mount = false;
5252
return;

src/components/side_views/TreeView.svelte

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,17 @@
6565
if (last_plugin !== plugin) {
6666
last_plugin = plugin;
6767
settings = json_clone(
68-
$state.snapshot(plugin.settings.views.side.tree),
68+
untrack(() => $state.snapshot(plugin.settings.views.side.tree)),
6969
);
7070
}
7171
});
7272
7373
$effect(() => {
74-
plugin.settings.views.side.tree = $state.snapshot(settings);
75-
untrack(() => void plugin.saveSettings());
74+
const tree_snapshot = $state.snapshot(settings);
75+
untrack(() => {
76+
plugin.settings.views.side.tree = tree_snapshot;
77+
void plugin.saveSettings();
78+
});
7679
});
7780
7881
let edge_field_labels = $derived(

src/main.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@ export enum BCEvent {
4343
}
4444

4545
export default class BreadcrumbsPlugin extends Plugin {
46-
settings!: BreadcrumbsSettings;
46+
get settings(): BreadcrumbsSettings {
47+
return reactive_settings.current;
48+
}
49+
set settings(value: BreadcrumbsSettings) {
50+
reactive_settings.init(value);
51+
}
4752
graph!: NoteGraph;
4853
api!: BCAPI;
4954
events!: Events;
@@ -305,8 +310,6 @@ export default class BreadcrumbsPlugin extends Plugin {
305310
((await this.loadData()) ?? {}) as BreadcrumbsSettings,
306311
DEFAULT_SETTINGS,
307312
);
308-
309-
reactive_settings.init(this.settings);
310313
}
311314

312315
private handleFileMenu(menu: Menu, file: TAbstractFile): void {
@@ -321,9 +324,7 @@ export default class BreadcrumbsPlugin extends Plugin {
321324

322325
async saveSettings() {
323326
reactive_settings.current.is_dirty = false;
324-
this.settings = reactive_settings.snapshot();
325-
326-
await this.saveData(this.settings);
327+
await this.saveData(reactive_settings.snapshot());
327328
}
328329

329330
async backup_old_settings(): Promise<void> {

src/views/matrix.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { WorkspaceLeaf } from "obsidian";
2-
import { ItemView } from "obsidian";
2+
import { debounce, ItemView } from "obsidian";
33
import MatrixComponent from "src/components/side_views/Matrix.svelte";
44
import { VIEW_IDS } from "src/const/views";
55
import type BreadcrumbsPlugin from "src/main";
@@ -8,7 +8,7 @@ import { mount, unmount } from "svelte";
88

99
export class MatrixView extends ItemView {
1010
plugin: BreadcrumbsPlugin;
11-
component!: ReturnType<typeof MatrixComponent>;
11+
component: ReturnType<typeof MatrixComponent> | undefined;
1212

1313
constructor(leaf: WorkspaceLeaf, plugin: BreadcrumbsPlugin) {
1414
super(leaf);
@@ -26,10 +26,9 @@ export class MatrixView extends ItemView {
2626
icon = "blinds";
2727

2828
onload(): void {
29+
const redraw = debounce(() => void this.onOpen(), 100);
2930
this.registerEvent(
30-
this.plugin.events.on(BCEvent.REDRAW_SIDE_VIEWS, () => {
31-
void this.onOpen();
32-
}),
31+
this.plugin.events.on(BCEvent.REDRAW_SIDE_VIEWS, redraw),
3332
);
3433
}
3534

@@ -45,7 +44,9 @@ export class MatrixView extends ItemView {
4544

4645
async onClose() {
4746
if (this.component) {
48-
await unmount(this.component);
47+
const old = this.component;
48+
this.component = undefined;
49+
await unmount(old);
4950
}
5051
}
5152
}

0 commit comments

Comments
 (0)