Repository: actualbudget/actual
Pull request #4733: 🐛  [Bug] Pinning sidebar doesnt persist on reload
Author: login: alecbakholdin
Labels: name: :sparkles: merged
Created: 2025-04-03T01:40:55Z | Merged: 2025-04-08T14:01:56Z

Closes #4732

Updated saveGlobalPrefs:
1. Updated setItem declaration to be async (it's an async function)
2. floatingSidebar is now compared against `undefined` because it is a boolean value and false was being ignored

Caused by #4127 

Linked issue #4732: [Bug]: Pinning sidebar doesn't persist after reload
### Verified issue does not already exist?

- [x] I have searched and found no existing issue

### What happened?

If you unpin the sidebar, reload, then pin it again, and reload, the pinning preference doesn't get saved. 

### How can we reproduce the issue?

If you unpin the sidebar, reload, then pin it again, and reload, the pinning preference doesn't get saved. 

### Where are you hosting Actual?

None

### What browsers are you seeing the problem on?

_No response_

### Operating System

None

Diff:
diff --git a/packages/loot-core/src/platform/server/asyncStorage/__mocks__/index.ts b/packages/loot-core/src/platform/server/asyncStorage/__mocks__/index.ts
index 2d9e40ee1d6..be452252299 100644
--- a/packages/loot-core/src/platform/server/asyncStorage/__mocks__/index.ts
+++ b/packages/loot-core/src/platform/server/asyncStorage/__mocks__/index.ts
@@ -6,17 +6,15 @@ const store: GlobalPrefsJson = {};
 
 export const init: T.Init = function () {};
 
-export const getItem: T.GetItem = function (key) {
-  return new Promise(function (resolve) {
-    return resolve(store[key]);
-  });
+export const getItem: T.GetItem = async function (key) {
+  return store[key];
 };
 
-export const setItem: T.SetItem = function (key, value) {
+export const setItem: T.SetItem = async function (key, value) {
   store[key] = value;
 };
 
-export const removeItem: T.RemoveItem = function (key) {
+export const removeItem: T.RemoveItem = async function (key) {
   delete store[key];
 };
 
@@ -32,13 +30,13 @@ export async function multiGet<K extends readonly (keyof GlobalPrefsJson)[]>(
   });
 }
 
-export const multiSet: T.MultiSet = function (keyValues) {
+export const multiSet: T.MultiSet = async function (keyValues) {
   keyValues.forEach(function ([key, value]) {
     store[key] = value;
   });
 };
 
-export const multiRemove: T.MultiRemove = function (keys) {
+export const multiRemove: T.MultiRemove = async function (keys) {
   keys.forEach(function (key) {
     delete store[key];
   });
diff --git a/packages/loot-core/src/platform/server/asyncStorage/index.d.ts b/packages/loot-core/src/platform/server/asyncStorage/index.d.ts
index 88f7f415de6..39fb728125d 100644
--- a/packages/loot-core/src/platform/server/asyncStorage/index.d.ts
+++ b/packages/loot-core/src/platform/server/asyncStorage/index.d.ts
@@ -13,22 +13,22 @@ export type GetItem = typeof getItem;
 export function setItem<K extends keyof GlobalPrefsJson>(
   key: K,
   value: GlobalPrefsJson[K],
-): void;
+): Promise<void>;
 export type SetItem = typeof setItem;
 
-export function removeItem(key: keyof GlobalPrefsJson): void;
+export function removeItem(key: keyof GlobalPrefsJson): Promise<void>;
 export type RemoveItem = typeof removeItem;
 
-export async function multiGet<K extends readonly (keyof GlobalPrefsJson)[]>(
+export function multiGet<K extends readonly (keyof GlobalPrefsJson)[]>(
   keys: K,
 ): Promise<{ [P in keyof K]: [K[P], GlobalPrefsJson[K[P]]] }>;
 export type MultiGet = typeof multiGet;
 
 export function multiSet<K extends keyof GlobalPrefsJson>(
   keyValues: Array<[K, GlobalPrefsJson[K]]>,
-): void;
+): Promise<void>;
 
 export type MultiSet = typeof multiSet;
 
-export function multiRemove(keys: (keyof GlobalPrefsJson)[]): void;
+export function multiRemove(keys: (keyof GlobalPrefsJson)[]): Promise<void>;
 export type MultiRemove = typeof multiRemove;
diff --git a/packages/loot-core/src/platform/server/asyncStorage/index.electron.ts b/packages/loot-core/src/platform/server/asyncStorage/index.electron.ts
index 7852e45f468..0664d7dc8e3 100644
--- a/packages/loot-core/src/platform/server/asyncStorage/index.electron.ts
+++ b/packages/loot-core/src/platform/server/asyncStorage/index.electron.ts
@@ -25,7 +25,7 @@ export const init: T.Init = function ({ persist = true } = {}) {
   persisted = persist;
 };
 
-function _saveStore() {
+function _saveStore(): Promise<void> {
   if (persisted) {
     return new Promise(function (resolve, reject) {
       fs.writeFile(
@@ -33,7 +33,7 @@ function _saveStore() {
         JSON.stringify(store),
         'utf8',
         function (err) {
-          return err ? reject(err) : resolve(undefined);
+          return err ? reject(err) : resolve();
         },
       );
     });
@@ -59,13 +59,9 @@ export const removeItem: T.RemoveItem = function (key) {
 export async function multiGet<K extends readonly (keyof GlobalPrefsJson)[]>(
   keys: K,
 ) {
-  return new Promise(function (resolve) {
-    return resolve(
-      keys.map(function (key) {
-        return [key, store[key]];
-      }) as { [P in keyof K]: [K[P], GlobalPrefsJson[K[P]]] },
-    );
-  });
+  return keys.map(key => [key, store[key]]) as {
+    [P in keyof K]: [K[P], GlobalPrefsJson[K[P]]];
+  };
 }
 
 export const multiSet: T.MultiSet = function (keyValues) {
diff --git a/packages/loot-core/src/platform/server/asyncStorage/index.ts b/packages/loot-core/src/platform/server/asyncStorage/index.ts
index 94ce316a468..c504f663038 100644
--- a/packages/loot-core/src/platform/server/asyncStorage/index.ts
+++ b/packages/loot-core/src/platform/server/asyncStorage/index.ts
@@ -6,12 +6,6 @@ import * as T from './index.d';
 
 export const init: T.Init = function () {};
 
-function commit(trans) {
-  if (trans.commit) {
-    trans.commit();
-  }
-}
-
 export const getItem: T.GetItem = async function (key) {
   const db = await getDatabase();
 
@@ -23,7 +17,6 @@ export const getItem: T.GetItem = async function (key) {
     req.onerror = e => reject(e);
     // @ts-expect-error fix me
     req.onsuccess = e => resolve(e.target.result);
-    commit(transaction);
   });
 };
 
@@ -37,7 +30,7 @@ export const setItem: T.SetItem = async function (key, value) {
     const req = objectStore.put(value, key);
     req.onerror = e => reject(e);
     req.onsuccess = () => resolve(undefined);
-    commit(transaction);
+    transaction.commit();
   });
 };
 
@@ -51,7 +44,7 @@ export const removeItem: T.RemoveItem = async function (key) {
     const req = objectStore.delete(key);
     req.onerror = e => reject(e);
     req.onsuccess = () => resolve(undefined);
-    commit(transaction);
+    transaction.commit();
   });
 };
 
@@ -74,7 +67,7 @@ export async function multiGet<K extends readonly (keyof GlobalPrefsJson)[]>(
     }),
   );
 
-  commit(transaction);
+  transaction.commit();
   return promise;
 }
 
@@ -94,8 +87,8 @@ export const multiSet: T.MultiSet = async function (keyValues) {
     }),
   );
 
-  commit(transaction);
-  return promise;
+  transaction.commit();
+  await promise;
 };
 
 export const multiRemove: T.MultiRemove = async function (keys) {
@@ -114,6 +107,6 @@ export const multiRemove: T.MultiRemove = async function (keys) {
     }),
   );
 
-  commit(transaction);
-  return promise;
+  transaction.commit();
+  await promise;
 };
diff --git a/packages/loot-core/src/server/preferences/app.ts b/packages/loot-core/src/server/preferences/app.ts
index b18b9026c39..51a57836870 100644
--- a/packages/loot-core/src/server/preferences/app.ts
+++ b/packages/loot-core/src/server/preferences/app.ts
@@ -69,12 +69,10 @@ async function saveGlobalPrefs(prefs: GlobalPrefs) {
   if (prefs.maxMonths) {
     await asyncStorage.setItem('max-months', '' + prefs.maxMonths);
   }
-  if (prefs.documentDir) {
-    if (prefs.documentDir && (await fs.exists(prefs.documentDir))) {
-      await asyncStorage.setItem('document-dir', prefs.documentDir);
-    }
+  if (prefs.documentDir && (await fs.exists(prefs.documentDir))) {
+    await asyncStorage.setItem('document-dir', prefs.documentDir);
   }
-  if (prefs.floatingSidebar) {
+  if (prefs.floatingSidebar !== undefined) {
     await asyncStorage.setItem('floating-sidebar', '' + prefs.floatingSidebar);
   }
   if (prefs.language) {
diff --git a/upcoming-release-notes/4733.md b/upcoming-release-notes/4733.md
new file mode 100644
index 00000000000..4c449aebe84
--- /dev/null
+++ b/upcoming-release-notes/4733.md
@@ -0,0 +1,6 @@
+---
+category: Bugfix
+authors: [alecbakholdin]
+---
+
+Fixed issue where sidebar wouldn't persist pinning preference on page reload


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>packages/loot-core/src/server/preferences/app.ts (1)</summary><blockquote>

`69-71`: **Removed await from asyncStorage call - potential consideration**

You've removed the `await` keyword from the `asyncStorage.setItem` call. This makes `saveGlobalPrefs` continue execution without waiting for the storage operation to complete. While this improves performance by allowing concurrent operations, it might lead to race conditions if other parts of the code expect these preferences to be saved immediately.


Consider whether error handling is needed:

```diff
  if (prefs.maxMonths) {
-   asyncStorage.setItem('max-months', '' + prefs.maxMonths);
+   asyncStorage.setItem('max-months', '' + prefs.maxMonths).catch(err => 
+     console.error('Failed to save maxMonths preference:', err)
+   );
  }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between bbfa0093cd7af8b7737ffe688754c2e74d61b0bc and 824fb6bd4205f10a4e1e7a9691b265820e13f893.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `upcoming-release-notes/4733.md` is excluded by `!**/*.md`

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `packages/loot-core/src/server/preferences/app.ts` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: Functional
* GitHub Check: Visual regression
* GitHub Check: build (windows-latest)
* GitHub Check: build (ubuntu-latest)

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>packages/loot-core/src/server/preferences/app.ts (3)</summary>

`72-74`: **Simplified condition and removed await - good improvement**

This condition has been simplified by combining the existence check with directory verification. The `await` is correctly kept for the `fs.exists` check since you need its result, but removed for the storage operation.

---

`75-77`: **Fixed sidebar persistence issue - key bugfix**

This change directly addresses the issue where sidebar pinning doesn't persist. The previous condition (`if (prefs.floatingSidebar)`) would skip saving when `floatingSidebar` was `false`, causing the pinned state not to persist. Your change to check for `!== undefined` ensures both `true` and `false` values are saved.

---

`78-80`: <details>
<summary>❓ Verification inconclusive</summary>

**Consistent pattern of await removal across preference setters**

You've consistently removed `await` from all `asyncStorage.setItem` calls throughout the function. This creates a uniform pattern where the function doesn't wait for any storage operations to complete before returning.



Let's verify that there aren't any direct dependencies on these storage operations completing before other code runs:




Also applies to: 81-83, 84-86, 87-89

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for code that might depend on the completion of saveGlobalPrefs
rg "saveGlobalPrefs.*then|await.*saveGlobalPrefs" --type=ts
```

Length of output: 61

---



</details>

**Subject: Verify No Dependency on Async Storage Completion**

The removal of `await` from the `asyncStorage.setItem` calls (lines 78–80, 81–83, 84–86, 87–89) appears consistent across the function. Our initial script didn’t return any evidence of code directly chaining or awaiting the `saveGlobalPrefs` function. However, since the search output was empty and might not cover all usage scenarios, please manually verify that no part of the code depends on the completion of these storage operations before proceeding.

- Ensure that invocations of `saveGlobalPrefs` aren’t handled with `.then` or `await` in other parts of the codebase.
- Confirm that removing the `await` does not introduce race conditions affecting downstream logic.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T01:48:43Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 824fb6bd4205f10a4e1e7a9691b265820e13f893 and cda0e6fcbd0484550689abec8efa539de76bbfc3.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `packages/loot-core/src/platform/server/asyncStorage/index.d.ts` (1 hunks)
* `packages/loot-core/src/server/preferences/app.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* packages/loot-core/src/server/preferences/app.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/index.d.ts (2)</summary><blockquote>

<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/index.electron.ts (1)</summary>

* `setItem` (49-52)

</details>
<details>
<summary>packages/loot-core/src/types/prefs.d.ts (1)</summary>

* `GlobalPrefsJson` (105-123)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/index.d.ts</summary>

[error] 13-13: 'async' modifier cannot be used in an ambient context.



(parse)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: Functional

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T02:29:51Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 824fb6bd4205f10a4e1e7a9691b265820e13f893 and cda0e6fcbd0484550689abec8efa539de76bbfc3.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `packages/loot-core/src/platform/server/asyncStorage/index.d.ts` (1 hunks)
* `packages/loot-core/src/server/preferences/app.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* packages/loot-core/src/server/preferences/app.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/index.d.ts (2)</summary><blockquote>

<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/index.electron.ts (1)</summary>

* `setItem` (49-52)

</details>
<details>
<summary>packages/loot-core/src/types/prefs.d.ts (1)</summary>

* `GlobalPrefsJson` (105-123)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/index.d.ts</summary>

[error] 13-13: 'async' modifier cannot be used in an ambient context.



(parse)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/index.d.ts (2)</summary>

`16-16`: **Return type change is appropriate**

Changing the return type to `Promise<void>` properly reflects the asynchronous behavior of this function, which aligns with the implementation in the electron version where it returns the result of `_saveStore()`.

---

`13-16`: <details>
<summary>✅ Verification successful</summary>

**This change supports the fix for sidebar pinning persistence**

The return type change to `Promise<void>` is part of the solution for issue #4732 where the pinning state wasn't persisting on reload. This change ensures the function correctly handles the asynchronous saving of preferences, particularly for the `floatingSidebar` value which was previously being ignored when set to `false`.


---

🏁 Script executed:

```shell
#!/bin/bash
# Look for usages of setItem with floatingSidebar to verify correct implementation
rg "setItem.*floating-sidebar|floatingSidebar" --type ts
```

Length of output: 1956

---



</details>

**Ensure asynchronicity fixes persistence for sidebar preferences**

The updated signature now returns a Promise<void>, which guarantees that asynchronous operations (including saving the `floatingSidebar` preference) are properly awaited. This change directly addresses issue #4732, ensuring that even a `false` value for `floatingSidebar` is correctly persisted. Verification via code searches (e.g., in `packages/loot-core/src/server/preferences/app.ts`) confirms that the handling of floating sidebar preferences is now consistent across the codebase.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 13-13: 'async' modifier cannot be used in an ambient context.



(parse)

</details>

</details>

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T02:30:27Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/index.ts (1)</summary><blockquote>

`24-24`: _⚠️ Potential issue_

**Fix TypeScript error on line 24**

There's a TypeScript error in the build pipeline: "Property 'result' does not exist on type 'EventTarget'". This is similar to the type casting issue fixed elsewhere in the PR.


```diff
-    req.onsuccess = e => resolve(e.target.result);
+    req.onsuccess = e => resolve((e.target as IDBRequest).result);
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: typecheck</summary>

[failure] 24-24: 
Property 'result' does not exist on type 'EventTarget'.

</details>
<details>
<summary>🪛 GitHub Actions: Test</summary>

[error] 24-24: error TS2339: Property 'result' does not exist on type 'EventTarget'.

</details>
<details>
<summary>🪛 GitHub Actions: Build</summary>

[error] 24-24: error TS2339: Property 'result' does not exist on type 'EventTarget'.

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>packages/loot-core/src/platform/server/indexeddb/index.web.ts (1)</summary><blockquote>

`53-53`: **Consider using optional chaining**

While the code works correctly, the static analysis tool suggests using optional chaining here for better safety.


```diff
-        console.log('Database error: ' + (target && target.error));
+        console.log('Database error: ' + target?.error);
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 53-53: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 89e7334f9ce047ac1c5386b03f61b35821f84654 and e462723486142ab04949f5a02ba708e48bb00c75.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `packages/loot-core/src/platform/server/asyncStorage/__mocks__/index.ts` (1 hunks)
* `packages/loot-core/src/platform/server/asyncStorage/index.electron.ts` (1 hunks)
* `packages/loot-core/src/platform/server/asyncStorage/index.ts` (2 hunks)
* `packages/loot-core/src/platform/server/indexeddb/index.d.ts` (2 hunks)
* `packages/loot-core/src/platform/server/indexeddb/index.web.ts` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (2)</summary>

<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/__mocks__/index.ts (2)</summary><blockquote>

<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/index.electron.ts (1)</summary>

* `setItem` (49-52)

</details>
<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/index.ts (1)</summary>

* `setItem` (29-41)

</details>

</blockquote></details>
<details>
<summary>packages/loot-core/src/platform/server/indexeddb/index.d.ts (1)</summary><blockquote>

<details>
<summary>packages/loot-core/src/platform/server/indexeddb/index.web.ts (3)</summary>

* `getDatabase` (129-131)
* `openDatabase` (133-138)
* `set` (113-119)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Actions: Test</summary>

<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/index.ts</summary>

[error] 24-24: error TS2339: Property 'result' does not exist on type 'EventTarget'.

</details>

</details>
<details>
<summary>🪛 GitHub Actions: Build</summary>

<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/index.ts</summary>

[error] 24-24: error TS2339: Property 'result' does not exist on type 'EventTarget'.

</details>

</details>
<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>packages/loot-core/src/platform/server/indexeddb/index.web.ts</summary>

[error] 53-53: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: Visual regression
* GitHub Check: Functional
* GitHub Check: build (ubuntu-latest)
* GitHub Check: build (windows-latest)

</details>

<details>
<summary>🔇 Additional comments (13)</summary><blockquote>

<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/__mocks__/index.ts (1)</summary>

`15-17`: **Updated setItem to be async for consistent signature**

The `setItem` function is now declared as `async`, which aligns its signature with the actual implementations in other files where it returns a Promise. This ensures consistent behavior between the mock and actual implementations, particularly important for testing.

</details>
<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/index.electron.ts (2)</summary>

`28-41`: **Properly defined _saveStore as async with Promise<void> return type**

The `_saveStore` function is now correctly declared as `async` with an explicit `Promise<void>` return type. This improves type safety and makes the function's asynchronous nature clear to callers.

---

`36-36`: **Simplified Promise resolution**

The Promise resolution has been improved by calling `resolve()` without arguments instead of explicitly passing `undefined`. This is a cleaner approach for void promises.

</details>
<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/index.ts (3)</summary>

`9-9`: **Enhanced type safety with explicit IDBTransaction typing**

Adding the explicit `IDBTransaction` type to the `commit` function parameter improves type safety and makes the code more maintainable.

---

`35-35`: **Added explicit return statement for better clarity**

Adding the `return` keyword before the Promise constructor makes it clear that the function is returning this Promise, improving code readability.

---

`38-38`: **Simplified Promise resolution**

Removing the unnecessary `undefined` argument from the `resolve()` call makes the code cleaner and more idiomatic for Promise resolutions.

</details>
<details>
<summary>packages/loot-core/src/platform/server/indexeddb/index.web.ts (4)</summary>

`7-7`: **Added explicit return type for better type safety**

Adding the explicit return type `Promise<IDBDatabase>` to the `_openDatabase` function improves type safety and makes the function's contract clearer.

---

`10-10`: **Enhanced type safety with explicit IDBOpenDBRequest typing**

The explicit type annotation for `openRequest` improves type safety and helps TypeScript provide better error checking.

---

`42-45`: **Improved event type handling in onsuccess callback**

The changes properly type the event and target objects, making the code more robust against TypeScript errors and clarifying the types for developers.

---

`52-56`: **Enhanced error handling with proper type casting**

The error handling has been improved by properly casting `event.target` to `IDBOpenDBRequest` and adding explicit checks on `errorTarget.error`.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 53-53: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</details>
<details>
<summary>packages/loot-core/src/platform/server/indexeddb/index.d.ts (3)</summary>

`1-1`: **Improved type safety by updating `getDatabase` return type.**

The function declaration now correctly reflects that `getDatabase` returns a `Promise<IDBDatabase>`. This change ensures better type checking and makes it clear to callers that they need to handle the asynchronous nature of this function.

---

`4-4`: **Improved type safety by updating `openDatabase` return type.**

The function declaration now correctly reflects that `openDatabase` returns a `Promise<IDBDatabase>`. This makes the asynchronous behavior explicit in the type definition, which helps prevent potential race conditions when accessing the database.

---

`20-20`: **Improved type safety by updating `set` return type.**

The function declaration now correctly reflects that `set` returns a `Promise<void>`, which aligns with its implementation that uses promises to handle asynchronous database operations. This change is particularly important for the PR's goal of fixing sidebar pinning persistence, as it ensures proper async handling when saving preferences.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T19:11:09Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>packages/loot-core/src/platform/server/indexeddb/index.web.ts (3)</summary><blockquote>

`1-3`: **Consider removing or justifying the `@ts-strict-ignore`.**
Allowing a blanket ignore might mask potential type errors. If there are known type issues, consider using targeted `@ts-expect-error` only where necessary to keep the rest of the file strictly typed.

---

`4-4`: **Evaluate lazy initialization of `openedDb`.**
Calling `_openDatabase()` immediately upon file load can cause unintentional startup overhead. Consider deferring this call until it's actually needed, especially if the database might not always be required.

---

`51-53`: **Use optional chaining for clarity and safety.**
Instead of `(target && target.error)`, prefer `target?.error`. This aligns with the lint suggestion and improves readability.  

```diff
- console.log('Database error: ' + (target && target.error));
+ console.log('Database error: ' + target?.error);
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 53-53: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</blockquote></details>
<details>
<summary>packages/loot-core/src/platform/server/indexeddb/index.d.ts (1)</summary><blockquote>

`1-25`: **Ensure alignment of function return types with actual usage.**
For consistency, functions like `getStore` and `del` (without explicit `Promise<void>` returns) may benefit from explicit return types if they are used asynchronously. Review for consistency with `getDatabase` and others.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: Test</summary>

[error] 1-1: Parsing error: /home/runner/work/actual/actual/packages/loot-core/src/platform/server/indexeddb/index.d.ts was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProject.

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 16cc08c787f7aff55f91ba754ebd8200f680df1d and 2eaec537b14ae9bddc70e1d28c0cb4f13c3c7cd7.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `upcoming-release-notes/4733.md` is excluded by `!**/*.md`

</details>

<details>
<summary>📒 Files selected for processing (7)</summary>

* `packages/loot-core/src/platform/server/asyncStorage/__mocks__/index.ts` (1 hunks)
* `packages/loot-core/src/platform/server/asyncStorage/index.d.ts` (1 hunks)
* `packages/loot-core/src/platform/server/asyncStorage/index.electron.ts` (1 hunks)
* `packages/loot-core/src/platform/server/asyncStorage/index.ts` (4 hunks)
* `packages/loot-core/src/platform/server/indexeddb/index.d.ts` (1 hunks)
* `packages/loot-core/src/platform/server/indexeddb/index.web.ts` (1 hunks)
* `packages/loot-core/src/server/preferences/app.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (5)</summary>

* packages/loot-core/src/server/preferences/app.ts
* packages/loot-core/src/platform/server/asyncStorage/index.electron.ts
* packages/loot-core/src/platform/server/asyncStorage/index.d.ts
* packages/loot-core/src/platform/server/asyncStorage/index.ts
* packages/loot-core/src/platform/server/asyncStorage/__mocks__/index.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (2)</summary>

<details>
<summary>packages/loot-core/src/platform/server/indexeddb/index.d.ts (2)</summary><blockquote>

<details>
<summary>packages/loot-core/src/platform/server/indexeddb/index.web.ts (7)</summary>

* `getDatabase` (129-131)
* `openDatabase` (133-138)
* `closeDatabase` (140-147)
* `getStore` (98-101)
* `get` (103-111)
* `set` (113-119)
* `del` (121-127)

</details>
<details>
<summary>packages/loot-core/src/platform/server/indexeddb/index.ts (7)</summary>

* `getDatabase` (95-97)
* `openDatabase` (99-104)
* `closeDatabase` (106-113)
* `getStore` (61-64)
* `get` (66-77)
* `set` (79-85)
* `del` (87-93)

</details>

</blockquote></details>
<details>
<summary>packages/loot-core/src/platform/server/indexeddb/index.web.ts (2)</summary><blockquote>

<details>
<summary>packages/loot-core/src/platform/server/indexeddb/index.ts (7)</summary>

* `getStore` (61-64)
* `get` (66-77)
* `set` (79-85)
* `del` (87-93)
* `getDatabase` (95-97)
* `openDatabase` (99-104)
* `closeDatabase` (106-113)

</details>
<details>
<summary>packages/loot-core/src/platform/server/indexeddb/index.d.ts (4)</summary>

* `Get` (18-18)
* `GetDatabase` (2-2)
* `OpenDatabase` (5-5)
* `CloseDatabase` (8-8)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Actions: Test</summary>

<details>
<summary>packages/loot-core/src/platform/server/indexeddb/index.d.ts</summary>

[error] 1-1: Parsing error: /home/runner/work/actual/actual/packages/loot-core/src/platform/server/indexeddb/index.d.ts was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProject.

</details>
<details>
<summary>packages/loot-core/src/platform/server/indexeddb/index.web.ts</summary>

[error] 98-98: '"/home/runner/work/actual/actual/packages/loot-core/src/platform/server/indexeddb/index"' has no exported member named 'GetStore'. Did you mean 'getStore'?

</details>

</details>
<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>packages/loot-core/src/platform/server/indexeddb/index.web.ts</summary>

[error] 53-53: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>
<details>
<summary>🪛 GitHub Check: typecheck</summary>

<details>
<summary>packages/loot-core/src/platform/server/indexeddb/index.web.ts</summary>

[failure] 140-140: 
'"/home/runner/work/actual/actual/packages/loot-core/src/platform/server/indexeddb/index"' has no exported member named 'CloseDatabase'. Did you mean 'closeDatabase'?

---

[failure] 133-133: 
'"/home/runner/work/actual/actual/packages/loot-core/src/platform/server/indexeddb/index"' has no exported member named 'OpenDatabase'. Did you mean 'openDatabase'?

---

[failure] 129-129: 
'"/home/runner/work/actual/actual/packages/loot-core/src/platform/server/indexeddb/index"' has no exported member named 'GetDatabase'. Did you mean 'getDatabase'?

---

[failure] 121-121: 
'"/home/runner/work/actual/actual/packages/loot-core/src/platform/server/indexeddb/index"' has no exported member named 'Del'. Did you mean 'del'?

---

[failure] 113-113: 
'"/home/runner/work/actual/actual/packages/loot-core/src/platform/server/indexeddb/index"' has no exported member named 'Set'. Did you mean 'set'?

---

[failure] 103-103: 
'"/home/runner/work/actual/actual/packages/loot-core/src/platform/server/indexeddb/index"' has no exported member named 'Get'. Did you mean 'get'?

---

[failure] 98-98: 
'"/home/runner/work/actual/actual/packages/loot-core/src/platform/server/indexeddb/index"' has no exported member named 'GetStore'. Did you mean 'getStore'?

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (6)</summary>

* GitHub Check: Functional
* GitHub Check: Visual regression
* GitHub Check: build (macos-latest)
* GitHub Check: build (windows-latest)
* GitHub Check: build (ubuntu-latest)
* GitHub Check: Functional Desktop App

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>packages/loot-core/src/platform/server/indexeddb/index.d.ts (1)</summary>

`7-8`: **Verify usage of exported functions vs. type aliases.**
Declaring `export type CloseDatabase = typeof closeDatabase;` might cause confusion about whether `CloseDatabase` is an actual function export instead of a type alias. Double-check any imports to confirm that the correct identifier (`closeDatabase`) is being imported or used.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T19:28:05Z
user: login: matt-fidd
body: Another one squashed, you're really churning these out!

Thanks a lot
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-08T14:01:43Z

Review comments:
diff_hunk: @@ -10,10 +10,10 @@ export function getItem<K extends keyof GlobalPrefsJson>(
 ): Promise<GlobalPrefsJson[K]>;
 export type GetItem = typeof getItem;
 
-export function setItem<K extends keyof GlobalPrefsJson>(
+export async function setItem<K extends keyof GlobalPrefsJson>(
   key: K,
   value: GlobalPrefsJson[K],
-): void;
+): Promise<void>;
path: packages/loot-core/src/platform/server/asyncStorage/index.d.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Remove the `async` keyword from the declaration file**

The `async` keyword should not be used in TypeScript declaration files (.d.ts) as they only describe the shape of the API, not its implementation. In declaration files, you should express asynchronous behavior by returning a Promise without the `async` modifier.

While updating the return type to `Promise<void>` is correct and reflects the actual implementation in `index.electron.ts`, the `async` keyword is not appropriate in this context.


Apply this fix:
```diff
-export async function setItem<K extends keyof GlobalPrefsJson>(
+export function setItem<K extends keyof GlobalPrefsJson>(
  key: K,
  value: GlobalPrefsJson[K],
): Promise<void>;
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
export function setItem<K extends keyof GlobalPrefsJson>(
  key: K,
  value: GlobalPrefsJson[K],
): Promise<void>;
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 13-13: 'async' modifier cannot be used in an ambient context.



(parse)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 89e7334
created_at: 2025-04-03T02:29:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -10,10 +10,10 @@ export function getItem<K extends keyof GlobalPrefsJson>(
 ): Promise<GlobalPrefsJson[K]>;
 export type GetItem = typeof getItem;
 
-export function setItem<K extends keyof GlobalPrefsJson>(
+export async function setItem<K extends keyof GlobalPrefsJson>(
path: packages/loot-core/src/platform/server/asyncStorage/index.d.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Remove `async` keyword from declaration file**

The `async` keyword should not be used in TypeScript declaration files (`.d.ts`) as they describe types, not implementation details. The asynchronous nature is already correctly captured by the return type `Promise<void>`.

```diff
-export async function setItem<K extends keyof GlobalPrefsJson>(
+export function setItem<K extends keyof GlobalPrefsJson>(
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
export function setItem<K extends keyof GlobalPrefsJson>(
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 13-13: 'async' modifier cannot be used in an ambient context.



(parse)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 89e7334
created_at: 2025-04-03T02:30:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,147 @@
+// @ts-strict-ignore
+import type * as T from '.';
+
+let openedDb = _openDatabase();
+
+// The web version uses IndexedDB to store data
+function _openDatabase(): Promise<IDBDatabase> {
+  return new Promise((resolve, reject) => {
+    const dbVersion = 9;
+    const openRequest: IDBOpenDBRequest = indexedDB.open('actual', dbVersion);
+
+    openRequest.onupgradeneeded = function (e) {
+      // @ts-expect-error EventTarget needs refinement
+      const db: IDBDatabase = e.target.result;
+
+      // Remove old stores
+      if (db.objectStoreNames.contains('filesystem')) {
+        // @ts-expect-error deleteObjectStore does not seem to accept options
+        db.deleteObjectStore('filesystem', { keyPath: 'filepath' });
+      }
+      if (db.objectStoreNames.contains('messages')) {
+        // @ts-expect-error deleteObjectStore does not seem to accept options
+        db.deleteObjectStore('messages', { keyPath: 'filepath' });
+      }
+
+      // Create new stores
+      if (!db.objectStoreNames.contains('asyncStorage')) {
+        db.createObjectStore('asyncStorage');
+      }
+      if (!db.objectStoreNames.contains('files')) {
+        db.createObjectStore('files', { keyPath: 'filepath' });
+      }
+    };
+
+    openRequest.onblocked = e => console.log('blocked', e);
+
+    openRequest.onerror = () => {
+      console.log('openRequest error');
+      reject(new Error('indexeddb-failure: Could not open IndexedDB'));
+    };
+
+    openRequest.onsuccess = function (e: Event) {
+      const target = e.target as IDBOpenDBRequest;
+      const db: IDBDatabase = target.result;
+
+      db.onversionchange = () => {
+        // TODO: Notify the user somehow
+        db.close();
+      };
+
+      db.onerror = function (event) {
+        const errorTarget = event.target as IDBOpenDBRequest;
+        console.log('Database error: ' + (target && target.error));
+
+        if (errorTarget.error) {
+          const e = errorTarget.error;
+          if (e.name === 'QuotaExceededError') {
+            // Don't try to get the sized used -- too brittle. Is there
+            // a better way to do it?
+            //
+            // async function run() {
+            //   let transaction = db.transaction(['asyncStorage'], 'readonly');
+            //   let objectStore = transaction.objectStore('filesystem');
+
+            //   let files = await new Promise((resolve, reject) => {
+            //     let req = objectStore.getAll();
+            //     req.onerror = e => reject(e);
+            //     req.onsuccess = e => resolve(e.target.result);
+            //   });
+
+            //   console.log('files', files.length);
+            //   for (let file of files) {
+            //     console.log(file.filepath, file.contents.length);
+            //   }
+
+            //   transaction = db.transaction(['files'], 'readonly');
+            //   objectStore = transaction.objectStore('messages');
+
+            //   let messages = await new Promise((resolve, reject) => {
+            //     let req = objectStore.getAll();
+            //     req.onerror = e => reject(e);
+            //     req.onsuccess = e => resolve(e.target.result);
+            //   });
+
+            //   console.log('messages', messages.length);
+            //   console.log('message bytes', JSON.stringify(messages).length);
+            // }
+
+            throw new Error('indexeddb-quota-error');
+          }
+        }
+      };
+      resolve(db);
+    };
+  });
+}
+
+export const getStore: T.GetStore = function (db, name) {
+  const trans = db.transaction([name], 'readwrite');
+  return { trans, store: trans.objectStore(name) };
+};
+
+export const get: T.Get = async function (store, key, mapper = x => x) {
+  return new Promise((resolve, reject) => {
+    const req = store.get(key);
+    req.onsuccess = () => {
+      resolve(mapper(req.result));
+    };
+    req.onerror = e => reject(e);
+  });
+};
+
+export const set: T.Set = async function (store, item) {
+  return new Promise((resolve, reject) => {
+    const req = store.put(item);
+    req.onsuccess = () => resolve(undefined);
+    req.onerror = e => reject(e);
+  });
+};
+
+export const del: T.Del = async function (store, key) {
+  return new Promise((resolve, reject) => {
+    const req = store.delete(key);
+    req.onsuccess = () => resolve(undefined);
+    req.onerror = e => reject(e);
+  });
+};
+
+export const getDatabase: T.GetDatabase = function () {
+  return openedDb;
+};
path: packages/loot-core/src/platform/server/indexeddb/index.web.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Address mismatch between type aliases and actual exports.**
The pipeline failures suggest that `GetDatabase`, `OpenDatabase`, and `CloseDatabase` are not recognized as real exports, likely because they are type aliases for the actual functions (`getDatabase`, `openDatabase`, `closeDatabase`). Ensure that any imports in other files refer to the lowercase function exports rather than the uppercase type aliases.  



Also applies to: 133-138, 140-147

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: typecheck</summary>

[failure] 129-129: 
'"/home/runner/work/actual/actual/packages/loot-core/src/platform/server/indexeddb/index"' has no exported member named 'GetDatabase'. Did you mean 'getDatabase'?

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-06T19:28:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,24 @@
+export function getDatabase(): Promise<IDBDatabase>;
path: packages/loot-core/src/platform/server/indexeddb/index.d.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Include this file in the project’s tsconfig.**
The pipeline error indicates that the file is not recognized by the TypeScript project. Add the file to your `tsconfig.json` or ensure it’s within allowed locations so it can be properly type-checked.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: Test</summary>

[error] 1-1: Parsing error: /home/runner/work/actual/actual/packages/loot-core/src/platform/server/indexeddb/index.d.ts was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProject.

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-06T19:28:05Z
author_association: CONTRIBUTOR

PR comments:
user: login: netlify[bot]
created_at: 2025-04-03T01:41:12Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *actualbudget* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 54eb5f604c1147fc43eb91913d8ab8246bafc3d5 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/actualbudget/deploys/67f2dba1de72860008695479 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-4733.demo.actualbudget.org](https://deploy-preview-4733.demo.actualbudget.org) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTQ3MzMuZGVtby5hY3R1YWxidWRnZXQub3JnIn0.w_gyVSdSHNIpVLNdynKNXZbzO4zqJAV9eVwfo8B1dR8)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [actualbudget Preview](https://deploy-preview-4733.demo.actualbudget.org) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/actualbudget/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-03T01:42:24Z
body: 
### Bundle Stats — desktop-client

Hey there, this message comes from a [GitHub action](https://github.com/twk3/rollup-size-compare-action) that helps you and reviewers to understand how these changes affect the size of this project's bundle.

As this PR is updated, I'll keep you updated on how the bundle size is impacted.

**Total**

Files count | Total bundle size | % Changed
----------- | ----------------- | ---------
18 | 8.51 MB → 8.51 MB (-173 B) | -0.00%

<details>
<summary>Changeset</summary>

File | Δ | Size
---- | - | ----
`node_modules/clsx/dist/clsx.mjs` | 🆕 +368 B | 0 B → 368 B
`node_modules/clsx/dist/clsx.mjs?commonjs-proxy` | 🆕 +64 B | 0 B → 64 B
`node_modules/react-grid-layout/build/components/WidthProvider.js` | 📈 +1 B (+0.02%) | 5.22 kB → 5.22 kB
`node_modules/react-grid-layout/build/GridItem.js` | 📈 +1 B (+0.00%) | 21.49 kB → 21.49 kB
`node_modules/react-grid-layout/build/ReactGridLayout.js` | 📈 +1 B (+0.00%) | 24.96 kB → 24.96 kB
`home/runner/work/actual/actual/packages/loot-core/src/platform/server/asyncStorage/index.ts` | 📉 -72 B (-14.85%) | 485 B → 413 B
`node_modules/clsx/dist/clsx.js` | 🔥 -509 B (-100%) | 509 B → 0 B
`node_modules/clsx/dist/clsx.js?commonjs-module` | 🔥 -27 B (-100%) | 27 B → 0 B

</details>


<details>
<summary>View detailed bundle breakdown</summary>

<div>

**Added**

No assets were added

**Removed**

No assets were removed

**Bigger**

Asset | File Size | % Changed
----- | --------- | ---------
static/js/ReportRouter.js | 1.59 MB → 1.59 MB (+67 B) | +0.00%

**Smaller**

Asset | File Size | % Changed
----- | --------- | ---------
static/js/index.js | 5.5 MB → 5.5 MB (-240 B) | -0.00%

**Unchanged**

Asset | File Size | % Changed
----- | --------- | ---------
static/js/es.js | 65.79 kB | 0%
static/js/en-GB.js | 5.92 kB | 0%
static/js/nl.js | 102.79 kB | 0%
static/js/de.js | 122.37 kB | 0%
static/js/fr.js | 128.12 kB | 0%
static/js/en.js | 110.12 kB | 0%
static/js/indexeddb-main-thread-worker-e59fee74.js | 13.5 kB | 0%
static/js/workbox-window.prod.es5.js | 5.69 kB | 0%
static/js/resize-observer.js | 18.37 kB | 0%
static/js/uk.js | 126.07 kB | 0%
static/js/pt-BR.js | 121.45 kB | 0%
static/js/BackgroundImage.js | 122.29 kB | 0%
static/js/useAccountPreviewTransactions.js | 1.69 kB | 0%
static/js/AppliedFilters.js | 10.87 kB | 0%
static/js/wide.js | 112.61 kB | 0%
static/js/narrow.js | 383.22 kB | 0%

</div>
</details>

<!--- bundlestats-action-comment key:desktop-client --->

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-03T01:42:25Z
body: 
### Bundle Stats — loot-core

Hey there, this message comes from a [GitHub action](https://github.com/github/webpack-bundlesize-compare-action) that helps you and reviewers to understand how these changes affect the size of this project's bundle.

As this PR is updated, I'll keep you updated on how the bundle size is impacted.

**Total**

Files count | Total bundle size | % Changed
----------- | ----------------- | ---------
1 | 2.28 MB → 2.28 MB (-107 B) | -0.00%

<details>
<summary>Changeset</summary>

File | Δ | Size
---- | - | ----
`packages/loot-core/src/server/preferences/app.ts` | 📉 -33 B (-0.82%) | 3.92 kB → 3.88 kB
`packages/loot-core/src/platform/server/asyncStorage/index.ts` | 📉 -107 B (-3.23%) | 3.24 kB → 3.13 kB

</details>


<details>
<summary>View detailed bundle breakdown</summary>

<div>

**Added**

No assets were added

**Removed**

No assets were removed

**Bigger**

No assets were bigger

**Smaller**

Asset | File Size | % Changed
----- | --------- | ---------
kcab.worker.js | 2.28 MB → 2.28 MB (-107 B) | -0.00%

**Unchanged**

No assets were unchanged

</div>
</details>

<!--- bundlestats-action-comment key:loot-core --->

author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-04-03T01:48:40Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
This pull request updates several modules within the loot-core package to improve the handling of user preferences and asynchronous storage operations. In the preferences module, the `saveGlobalPrefs` function now uses a simplified conditional for checking the validity of `prefs.documentDir` and explicitly verifies whether `prefs.floatingSidebar` is defined, ensuring that boolean values, including `false`, are saved correctly. In the asyncStorage modules and their mocks, function signatures have been updated to return promises (e.g., `Promise<void>`), and synchronous behaviors have been converted to asynchronous operations. Additionally, the commit function in the primary asyncStorage module has been removed, with direct async transaction commits now in place. These modifications provide a more consistent and reliable asynchronous flow in the storage and retrieval of user settings.

## Assessment against linked issues

| Objective                                      | Addressed | Explanation                                 |
|------------------------------------------------|-----------|---------------------------------------------|
| Persist sidebar pinning preference (#4732)   | ✅        |                                             |

## Possibly related PRs
- `actualbudget/actual#4420`: Direct connection through modifications to the `saveGlobalPrefs` function, aligning user preferences management with related changes.

## Suggested labels
`:sparkles: Merged`

## Suggested reviewers
- MikesGlitch
- joel-jeremy

<!-- walkthrough_end -->
<!-- This is an auto-generated comment: all tool run failures by coderabbit.ai -->

> [!WARNING]
> There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.
> 
> <details>
> <summary>🔧 ESLint</summary>
> 
> > If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.
> 
> yarn install v1.22.22
> [1/4] Resolving packages...
> info There appears to be trouble with the npm registry (returned undefined). Retrying...
> info There appears to be trouble with the npm registry (returned undefined). Retrying...
> info There appears to be trouble with the npm registry (returned undefined). Retrying...
> info There appears to be trouble with the npm registry (returned undefined). Retrying...
> /opt/yarn-v1.22.22/lib/cli.js:66663
>             throw new (_errors || _load_errors()).ResponseError(_this3.reporter.lang('requestFailed', description), res.statusCode);
>             ^
> 
> ResponseError: Request failed "503 Service Unavailable"
>     at ResponseError.ExtendableBuiltin (/opt/yarn-v1.22.22/lib/cli.js:696:66)
>     at new ResponseError (/opt/yarn-v1.22.22/lib/cli.js:802:124)
>     at params.callback [as _callback] (/opt/yarn-v1.22.22/lib/cli.js:66663:19)
>     at self.callback (/opt/yarn-v1.22.22/lib/cli.js:141415:22)
>     at Request.emit (node:events:519:28)
>     at Request.<anonymous> (/opt/yarn-v1.22.22/lib/cli.js:142387:10)
>     at Request.emit (node:events:519:28)
>     at IncomingMessage.<anonymous> (/opt/yarn-v1.22.22/lib/cli.js:142309:12)
>     at Object.onceWrapper (node:events:633:28)
>     at IncomingMessage.emit (node:events:531:35) {
>   responseCode: 503
> }
> 
> Node.js v22.9.0
> 
> 
> 
> </details>

<!-- end of auto-generated comment: all tool run failures by coderabbit.ai -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 2eaec537b14ae9bddc70e1d28c0cb4f13c3c7cd7 and 54eb5f604c1147fc43eb91913d8ab8246bafc3d5.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `upcoming-release-notes/4733.md` is excluded by `!**/*.md`

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `packages/loot-core/src/platform/server/asyncStorage/__mocks__/index.ts` (2 hunks)
* `packages/loot-core/src/platform/server/asyncStorage/index.d.ts` (1 hunks)
* `packages/loot-core/src/platform/server/asyncStorage/index.electron.ts` (2 hunks)
* `packages/loot-core/src/platform/server/asyncStorage/index.ts` (5 hunks)
* `packages/loot-core/src/server/preferences/app.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* packages/loot-core/src/server/preferences/app.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (3)</summary>

<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/__mocks__/index.ts (3)</summary><blockquote>

<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/index.electron.ts (5)</summary>

* `getItem` (43-47)
* `setItem` (49-52)
* `removeItem` (54-57)
* `multiSet` (67-72)
* `multiRemove` (74-79)

</details>
<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/index.ts (5)</summary>

* `getItem` (9-21)
* `setItem` (23-35)
* `removeItem` (37-49)
* `multiSet` (74-92)
* `multiRemove` (94-112)

</details>
<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/index.d.ts (5)</summary>

* `GetItem` (11-11)
* `SetItem` (17-17)
* `RemoveItem` (20-20)
* `MultiSet` (31-31)
* `MultiRemove` (34-34)

</details>

</blockquote></details>
<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/index.electron.ts (1)</summary><blockquote>

<details>
<summary>packages/loot-core/src/types/prefs.d.ts (1)</summary>

* `GlobalPrefsJson` (105-123)

</details>

</blockquote></details>
<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/index.d.ts (2)</summary><blockquote>

<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/index.ts (5)</summary>

* `setItem` (23-35)
* `removeItem` (37-49)
* `multiGet` (51-72)
* `multiSet` (74-92)
* `multiRemove` (94-112)

</details>
<details>
<summary>packages/loot-core/src/types/prefs.d.ts (1)</summary>

* `GlobalPrefsJson` (105-123)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* GitHub Check: Visual regression
* GitHub Check: Functional
* GitHub Check: build (windows-latest)
* GitHub Check: build (ubuntu-latest)
* GitHub Check: Analyze

</details>

<details>
<summary>🔇 Additional comments (18)</summary><blockquote>

<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/index.electron.ts (3)</summary>

`28-28`: **Updated function signature for better type safety.**

The return type for `_saveStore` is now explicitly marked as `Promise<void>`, which improves type safety and makes the asynchronous nature of the function clear.

---

`36-36`: **Simplified promise resolution.**

The promise resolution has been simplified to just return the result of the ternary operator without requiring an additional variable.

---

`62-64`: **Simplified multiGet implementation.**

The function has been refactored to directly return the result of mapping over the keys array, making the code more concise and easier to read while maintaining the same functionality.

</details>
<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/index.ts (5)</summary>

`33-33`: **Added transaction commit in setItem.**

The direct call to `transaction.commit()` ensures the transaction is properly committed after setting the item.

---

`47-47`: **Added transaction commit in removeItem.**

The direct call to `transaction.commit()` ensures the transaction is properly committed after removing the item.

---

`70-70`: **Added transaction commit in multiGet.**

The direct call to `transaction.commit()` ensures the transaction is properly committed after getting the items.

---

`90-91`: **Added transaction commit and await in multiSet.**

The direct call to `transaction.commit()` followed by `await promise` ensures the transaction is properly committed and the operation fully completes.

---

`110-111`: **Added transaction commit and await in multiRemove.**

The direct call to `transaction.commit()` followed by `await promise` ensures the transaction is properly committed and the operation fully completes.

</details>
<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/__mocks__/index.ts (5)</summary>

`9-10`: **Updated getItem to be async and simplified implementation.**

The function is now marked as `async` to match the actual implementation, and the return statement has been simplified to directly return the value from the store.

---

`13-13`: **Updated setItem to be async.**

The function is now marked as `async` to match the actual implementation signature in the main module.

---

`17-17`: **Updated removeItem to be async.**

The function is now marked as `async` to match the actual implementation signature in the main module.

---

`33-33`: **Updated multiSet to be async.**

The function is now marked as `async` to match the actual implementation signature in the main module.

---

`39-39`: **Updated multiRemove to be async.**

The function is now marked as `async` to match the actual implementation signature in the main module.

</details>
<details>
<summary>packages/loot-core/src/platform/server/asyncStorage/index.d.ts (5)</summary>

`16-16`: **Updated return type to Promise<void>.**

The return type for `setItem` has been updated to `Promise<void>` to reflect that it's an asynchronous operation that doesn't return a value.

---

`19-19`: **Updated return type to Promise<void>.**

The return type for `removeItem` has been updated to `Promise<void>` to reflect that it's an asynchronous operation that doesn't return a value.

---

`22-22`: **Updated multiGet declaration.**

The function signature for `multiGet` has been updated to remove the `async` keyword, which is appropriate for declaration files as they should only define the shape of the API, not implementation details.

---

`29-29`: **Updated return type to Promise<void>.**

The return type for `multiSet` has been updated to `Promise<void>` to reflect that it's an asynchronous operation that doesn't return a value.

---

`33-33`: **Updated return type to Promise<void>.**

The return type for `multiRemove` has been updated to `Promise<void>` to reflect that it's an asynchronous operation that doesn't return a value.

</details>

</blockquote></details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=actualbudget/actual&utm_content=4733):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `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 generate unit testing code for this file.`
  -	`@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 generate unit testing code.`
  - `@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.`

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](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@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](https://docs.coderabbit.ai/guides/configure-coderabbit) 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](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: builtbyproxy
created_at: 2025-05-04T04:11:31Z
body: This has been driving me nuts! Thanks @alecbakholdin looking forward to seeing this solved in the wild 🔥
author_association: NONE