Skip to content

[WIP] Use JS Proxy to simplify loot-core app calls from desktop-client#7246

Open
joel-jeremy wants to merge 10 commits intomasterfrom
js-proxy
Open

[WIP] Use JS Proxy to simplify loot-core app calls from desktop-client#7246
joel-jeremy wants to merge 10 commits intomasterfrom
js-proxy

Conversation

@joel-jeremy
Copy link
Member

@joel-jeremy joel-jeremy commented Mar 20, 2026

Description

Note to reviewer: This is easier to review by commits

Currently, the desktop-client communicates with the loot-core server by calling a generic send function with a string handler name:

send('createPayee', { name: 'Landlord' })

This PR introduces a proxy-based server object that makes these calls look like regular method calls:

// internally calls `send` via proxy
server.createPayee({ name: 'Landlord' })

The same pattern is also applied within loot-core itself, so internal code no longer needs to call runHandler directly.

// before
await runHandler(app.handlers['createPayee'])

// after (internally calls runHandler via proxy)
await app.createPayee({ name: 'Landlord' })

Migration plan

The migration will be done incrementally, one server app at a time — for example, one PR for the budget folder, another for accounts, and so on. This keeps each PR focused and reviewable.

As part of each migration PR, handler names will be renamed from kebab-case to camelCase so the proxy methods are idiomatic JavaScript:

// before
server['close-budget']()

// after
server.closeBudget()

Payees app handlers are already renamed to camelCase on this PR.

This is groundwork for making the api package cross-platform (usable from desktop-client directly), since the proxy surface provides a clean, typed interface without leaking transport details.

Related issue(s)

Testing

Checklist

  • Release notes added (see link above)
  • No obvious regressions in affected areas
  • Self-review has been performed - I understand what each change in the code does and why it is needed

Bundle Stats

Bundle Files count Total bundle size % Changed
desktop-client 26 11.99 MB → 11.99 MB (-21 B) -0.00%
loot-core 1 4.83 MB → 4.83 MB (-562 B) -0.01%
api 4 4.06 MB → 4.06 MB (-500 B) -0.01%
cli 1 7.88 MB 0%
View detailed bundle stats

desktop-client

Total

Files count Total bundle size % Changed
26 11.99 MB → 11.99 MB (-21 B) -0.00%
Changeset
File Δ Size
home/runner/work/actual/actual/packages/loot-core/src/platform/client/connection/index.browser.ts 📈 +319 B (+9.41%) 3.31 kB → 3.62 kB
src/components/modals/MergeUnusedPayeesModal.tsx 📉 -2 B (-0.03%) 7.31 kB → 7.31 kB
src/components/ManageRules.tsx 📉 -4 B (-0.03%) 13.72 kB → 13.72 kB
src/components/mobile/payees/MobilePayeeEditPage.tsx 📉 -3 B (-0.05%) 5.97 kB → 5.97 kB
src/payees/mutations.ts 📉 -2 B (-0.09%) 2.08 kB → 2.08 kB
src/components/payees/ManagePayeesWithData.tsx 📉 -5 B (-0.17%) 2.84 kB → 2.84 kB
src/components/mobile/payees/MobilePayeesPage.tsx 📉 -7 B (-0.23%) 2.94 kB → 2.93 kB
src/payees/queries.ts 📉 -5 B (-0.26%) 1.91 kB → 1.9 kB
src/payees/location-adapters.ts 📉 -12 B (-0.99%) 1.18 kB → 1.17 kB
View detailed bundle breakdown

Added
No assets were added

Removed
No assets were removed

Bigger
No assets were bigger

Smaller

Asset File Size % Changed
static/js/index.js 3.23 MB → 3.23 MB (-11 B) -0.00%
static/js/narrow.js 354.12 kB → 354.11 kB (-10 B) -0.00%

Unchanged

Asset File Size % Changed
static/js/BackgroundImage.js 119.98 kB 0%
static/js/FormulaEditor.js 846.44 kB 0%
static/js/ReportRouter.js 1021.25 kB 0%
static/js/TransactionList.js 81.29 kB 0%
static/js/ca.js 185.57 kB 0%
static/js/da.js 104.66 kB 0%
static/js/de.js 177.58 kB 0%
static/js/en-GB.js 7.16 kB 0%
static/js/en.js 170.68 kB 0%
static/js/es.js 172.13 kB 0%
static/js/fr.js 177.57 kB 0%
static/js/indexeddb-main-thread-worker-e59fee74.js 13.46 kB 0%
static/js/it.js 168.97 kB 0%
static/js/nb-NO.js 154.72 kB 0%
static/js/nl.js 111.58 kB 0%
static/js/pl.js 88.34 kB 0%
static/js/pt-BR.js 180.5 kB 0%
static/js/resize-observer.js 18.03 kB 0%
static/js/th.js 179.94 kB 0%
static/js/theme.js 30.68 kB 0%
static/js/uk.js 213.14 kB 0%
static/js/useTransactionBatchActions.js 4.29 MB 0%
static/js/wide.js 418 B 0%
static/js/workbox-window.prod.es5.js 7.28 kB 0%

loot-core

Total

Files count Total bundle size % Changed
1 4.83 MB → 4.83 MB (-562 B) -0.01%
Changeset
File Δ Size
home/runner/work/actual/actual/packages/loot-core/src/server/app.ts 📈 +715 B (+50.93%) 1.37 kB → 2.07 kB
home/runner/work/actual/actual/packages/loot-core/src/server/mutators.ts 📈 +134 B (+6.46%) 2.03 kB → 2.16 kB
home/runner/work/actual/actual/packages/loot-core/src/server/importers/ynab4.ts 📈 +53 B (+0.55%) 9.35 kB → 9.4 kB
home/runner/work/actual/actual/packages/loot-core/src/server/sync/index.ts 📈 +13 B (+0.09%) 13.74 kB → 13.76 kB
home/runner/work/actual/actual/packages/loot-core/src/server/importers/ynab5.ts 📈 +23 B (+0.09%) 24.49 kB → 24.51 kB
home/runner/work/actual/actual/packages/loot-core/src/platform/server/fs/index.ts 📉 -2 B (-0.02%) 7.91 kB → 7.91 kB
home/runner/work/actual/actual/packages/loot-core/src/server/schedules/app.ts 📉 -4 B (-0.03%) 11.84 kB → 11.84 kB
home/runner/work/actual/actual/packages/loot-core/src/server/undo.ts 📉 -2 B (-0.04%) 4.81 kB → 4.81 kB
home/runner/work/actual/actual/packages/loot-core/src/server/budgetfiles/backups.ts 📉 -2 B (-0.04%) 4.73 kB → 4.72 kB
home/runner/work/actual/actual/packages/loot-core/src/server/transactions/index.ts 📉 -2 B (-0.06%) 3.28 kB → 3.28 kB
home/runner/work/actual/actual/packages/loot-core/src/server/budgetfiles/app.ts 📉 -8 B (-0.07%) 10.77 kB → 10.76 kB
home/runner/work/actual/actual/packages/loot-core/src/server/accounts/app.ts 📉 -18 B (-0.08%) 21.6 kB → 21.58 kB
home/runner/work/actual/actual/packages/loot-core/src/server/util/budget-name.ts 📉 -1 B (-0.09%) 1.14 kB → 1.14 kB
home/runner/work/actual/actual/packages/loot-core/src/server/sync/reset.ts 📉 -2 B (-0.13%) 1.45 kB → 1.45 kB
home/runner/work/actual/actual/packages/loot-core/src/platform/server/connection/index.ts 📉 -5 B (-0.16%) 3.06 kB → 3.05 kB
home/runner/work/actual/actual/packages/loot-core/src/server/importers/index.ts 📉 -3 B (-0.32%) 934 B → 931 B
home/runner/work/actual/actual/packages/loot-core/src/server/importers/actual.ts 📉 -3 B (-0.42%) 710 B → 707 B
home/runner/work/actual/actual/packages/loot-core/src/platform/server/fetch/index.ts 📉 -2 B (-0.53%) 374 B → 372 B
home/runner/work/actual/actual/packages/loot-core/src/mocks/budget.ts 📉 -171 B (-0.82%) 20.34 kB → 20.17 kB
home/runner/work/actual/actual/packages/loot-core/src/server/api.ts 📉 -507 B (-2.20%) 22.53 kB → 22.03 kB
home/runner/work/actual/actual/packages/loot-core/src/server/main.ts 📉 -165 B (-3.47%) 4.64 kB → 4.48 kB
home/runner/work/actual/actual/packages/loot-core/src/server/payees/app.ts 📉 -354 B (-5.85%) 5.91 kB → 5.57 kB
home/runner/work/actual/actual/packages/loot-core/src/server/main-app.ts 🔥 -249 B (-100%) 249 B → 0 B
View detailed bundle breakdown

Added

Asset File Size % Changed
kcab.worker.D1FeFhuH.js 0 B → 4.83 MB (+4.83 MB) -

Removed

Asset File Size % Changed
kcab.worker.Dmj0rSrb.js 4.83 MB → 0 B (-4.83 MB) -100%

Bigger
No assets were bigger

Smaller
No assets were smaller

Unchanged
No assets were unchanged


api

Total

Files count Total bundle size % Changed
4 4.06 MB → 4.06 MB (-500 B) -0.01%
Changeset
File Δ Size
home/runner/work/actual/actual/packages/loot-core/src/server/app.ts 📈 +697 B (+55.72%) 1.22 kB → 1.9 kB
home/runner/work/actual/actual/packages/loot-core/src/server/mutators.ts 📈 +130 B (+6.52%) 1.95 kB → 2.07 kB
home/runner/work/actual/actual/packages/loot-core/src/server/main.ts 📈 +71 B (+2.01%) 3.44 kB → 3.51 kB
home/runner/work/actual/actual/packages/loot-core/src/server/importers/ynab4.ts 📈 +23 B (+0.24%) 9.17 kB → 9.2 kB
home/runner/work/actual/actual/packages/loot-core/src/server/sync/index.ts 📈 +15 B (+0.11%) 13.35 kB → 13.37 kB
home/runner/work/actual/actual/packages/loot-core/src/server/accounts/app.ts 📉 -8 B (-0.04%) 21.31 kB → 21.3 kB
home/runner/work/actual/actual/packages/loot-core/src/server/budgetfiles/app.ts 📉 -6 B (-0.06%) 10.45 kB → 10.45 kB
home/runner/work/actual/actual/packages/loot-core/src/server/importers/ynab5.ts 📉 -17 B (-0.07%) 24.12 kB → 24.1 kB
home/runner/work/actual/actual/packages/loot-core/src/server/util/budget-name.ts 📉 -1 B (-0.09%) 1.12 kB → 1.12 kB
home/runner/work/actual/actual/packages/loot-core/src/server/importers/index.ts 📉 -3 B (-0.33%) 913 B → 910 B
home/runner/work/actual/actual/packages/loot-core/src/server/importers/actual.ts 📉 -3 B (-0.43%) 700 B → 697 B
home/runner/work/actual/actual/packages/loot-core/src/mocks/budget.ts 📉 -171 B (-0.85%) 19.71 kB → 19.55 kB
home/runner/work/actual/actual/packages/loot-core/src/server/api.ts 📉 -489 B (-2.17%) 21.96 kB → 21.48 kB
home/runner/work/actual/actual/packages/loot-core/src/server/payees/app.ts 📉 -312 B (-5.22%) 5.84 kB → 5.54 kB
home/runner/work/actual/actual/packages/loot-core/src/server/main-app.ts 🔥 -426 B (-100%) 426 B → 0 B
View detailed bundle breakdown

Added
No assets were added

Removed
No assets were removed

Bigger
No assets were bigger

Smaller

Asset File Size % Changed
index.js 3.84 MB → 3.84 MB (-500 B) -0.01%

Unchanged

Asset File Size % Changed
from-Bl-Hslp4.js 167.73 kB 0%
multipart-parser-BnDysoMr.js 8.1 kB 0%
src-iMkUmuwR.js 43.64 kB 0%

cli

Total

Files count Total bundle size % Changed
1 7.88 MB 0%
View detailed bundle breakdown

Added
No assets were added

Removed
No assets were removed

Bigger
No assets were bigger

Smaller
No assets were smaller

Unchanged

Asset File Size % Changed
cli.js 7.88 MB 0%

…nside app and update all calls to go through the mainApp for consistency.
…aturally i.e. app.createPayee(...) vs. app.runHandler('createPayee', ...)
@actual-github-bot actual-github-bot bot changed the title Js proxy [WIP] Js proxy Mar 20, 2026
@netlify
Copy link

netlify bot commented Mar 20, 2026

Deploy Preview for actualbudget ready!

Name Link
🔨 Latest commit 6494d8e
🔍 Latest deploy log https://app.netlify.com/projects/actualbudget/deploys/69bdd846f04d1c000812c90d
😎 Deploy Preview https://deploy-preview-7246.demo.actualbudget.org
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@joel-jeremy joel-jeremy changed the title [WIP] Js proxy [WIP] Use JS Proxy to simplify loot-core app calls Mar 20, 2026
@coderabbitai coderabbitai bot added the API Issues with the @actual-app/api package label Mar 20, 2026
@joel-jeremy joel-jeremy changed the title [WIP] Use JS Proxy to simplify loot-core app calls [WIP] Use JS Proxy to simplify loot-core app calls from desktop-client Mar 20, 2026
@github-actions

This comment has been minimized.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces ad-hoc string-based send() RPCs with a typed client server proxy and restructures server internals around a public App/createApp model; updates connection init/dispatch, incoming message handling, and many call sites to use server.* or mainApp/app.runHandler instead of string-keyed handlers.

Changes

Cohort / File(s) Summary
Client payee & location calls
packages/desktop-client/src/components/ManageRules.tsx, packages/desktop-client/src/components/mobile/payees/MobilePayeeEditPage.tsx, packages/desktop-client/src/components/mobile/payees/MobilePayeesPage.tsx, packages/desktop-client/src/components/modals/MergeUnusedPayeesModal.tsx, packages/desktop-client/src/components/payees/ManagePayeesWithData.tsx, packages/desktop-client/src/payees/location-adapters.ts, packages/desktop-client/src/payees/mutations.ts, packages/desktop-client/src/payees/queries.ts
Replaced send(...) RPC calls with typed server.* method calls (e.g., getPayeeRules, batchChangePayees, mergePayees, createPayee, createPayeeLocation, getNearbyPayees). Call shapes and surrounding control flow preserved.
Client connection implementation
packages/loot-core/src/platform/client/connection/index.browser.ts, packages/loot-core/src/platform/client/connection/index.ts, packages/loot-core/src/platform/client/connection/index-types.ts
Added exported server Proxy that forwards property calls to send(...), guards against thenable/symbol access, made init() idempotent via cached initPromise, and introduced ServerProxy type with init() returning Promise<ServerProxy>.
Server App & handler surface
packages/loot-core/src/server/app.ts, packages/loot-core/src/server/main.ts, packages/loot-core/src/server/main-app.ts (deleted), packages/loot-core/src/server/api.ts, packages/loot-core/src/server/payees/app.ts, packages/loot-core/src/server/mutators.ts
Converted to a public App<THandlers> class and createApp(handlers?) returning a Proxy-backed app; added getHandler/hasHandler/runHandler; migrated string-keyed registrations to typed handler registries; removed old mutable handlers export and deleted legacy main-app.ts; adjusted mutator/runHandler typing/execution.
Server connection & incoming message handling
packages/loot-core/src/platform/server/connection/index.ts, packages/loot-core/src/platform/server/connection/index.electron.ts, packages/loot-core/src/platform/server/connection/index-types.ts
Changed server-side connection init to accept app: App<...>; incoming messages validate via app.hasHandler() and dispatch via app.runHandler() / app.getHandler(); mutating checks use app.getHandler().
Importers, sync, and server modules
packages/loot-core/src/server/importers/*, packages/loot-core/src/server/sync/index.ts, packages/loot-core/src/server/budgetfiles/app.ts, packages/loot-core/src/server/accounts/app.ts, packages/loot-core/src/server/importers/ynab5.ts, packages/loot-core/src/server/importers/ynab4.ts
Repointed internal calls from handlers[...] / send(...) to mainApp[...] or mainApp.runHandler(...); event emission now uses mainApp.events; some importer query routing adjusted (ynab5 → aqlQuery).
Mocks, tests, and types
packages/loot-core/src/mocks/budget.ts, packages/loot-core/src/server/main.test.ts, packages/loot-core/src/server/api.test.ts, packages/loot-core/src/server/accounts/app-bank-sync.test.ts, packages/loot-core/src/types/app.ts, packages/loot-core/src/types/handlers.ts, packages/loot-core/src/types/server-handlers.ts
Updated mocks/tests to accept/use App instances and call app[...]/app.runHandler; exported App type added; removed/renamed server-handlers types and adjusted handler-type composition (e.g., PayeesHandlersPayeeHandlers).
Misc client/server call-site updates
multiple desktop-client and server files (see summary)
Numerous small call-site updates replacing send(...) with server.* or mainApp[...]; preserved existing error/undo/notification flows.
Release notes
upcoming-release-notes/7246.md
Added release-note entry describing migration to typed server proxy methods.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client (UI)
    participant Proxy as ServerProxy
    participant Conn as Connection
    participant ServerApp as mainApp
    participant Handler as Handler

    rect rgba(120,180,240,0.5)
        Note over Client,Handler: Client → typed proxy → connection → server app → handler
        Client->>Proxy: server.createPayee({ name })
        Proxy->>Conn: send('createPayee', { name })
        Conn->>ServerApp: deliver message
        ServerApp->>Handler: runHandler('createPayee', args)
        Handler-->>ServerApp: result
        ServerApp-->>Conn: reply(result)
        Conn-->>Proxy: resolve promise
        Proxy-->>Client: result
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Suggested labels

API

Poem

🐇 I hopped through handlers, tidy and spry,
Swapped scattered send() calls for server.* on the fly.
Proxies hum, apps now answer in line,
Events march neatly, handlers align.
The rabbit cheers: clean surface, bright and spry!

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: introducing a JS Proxy pattern to simplify app calls, replacing string-based send() calls with typed proxy methods.
Description check ✅ Passed The pull request description clearly explains the refactoring: replacing generic send() calls with a proxy-based server object for cleaner, typed method calls.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch js-proxy

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
packages/loot-core/src/server/importers/ynab5.ts (1)

451-461: ⚠️ Potential issue | 🟠 Major

Create imported payees sequentially.

This still fans out api/payee-create through Promise.all(). The payee creation path has already hit duplicate-payee races under concurrent processing, so importing payees in parallel can recreate that bug when names collide after normalization.

🐛 Proposed fix
-function importPayees(data: Budget, entityIdMap: Map<string, string>) {
-  return Promise.all(
-    data.payees.map(async payee => {
-      if (!payee.deleted) {
-        const id = await mainApp['api/payee-create']({
-          payee: { name: payee.name },
-        });
-        entityIdMap.set(payee.id, id);
-      }
-    }),
-  );
+async function importPayees(data: Budget, entityIdMap: Map<string, string>) {
+  for (const payee of data.payees) {
+    if (payee.deleted) {
+      continue;
+    }
+
+    const id = await mainApp['api/payee-create']({
+      payee: { name: payee.name },
+    });
+    entityIdMap.set(payee.id, id);
+  }
 }
Based on learnings: "When finalizing transactions that involve inserting or retrieving payees, avoid using `Promise.all` as it may result in duplicate payees due to concurrent operations. Sequential processing ensures payees are correctly handled without duplication."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/loot-core/src/server/importers/ynab5.ts` around lines 451 - 461,
importPayees currently fans out api/payee-create with Promise.all causing
duplicate-payee races; change it to process payees sequentially by iterating
(e.g., for...of) over data.payees, skipping deleted, awaiting each
mainApp['api/payee-create'] call before proceeding, and setting
entityIdMap.set(payee.id, id) immediately after each awaited call to ensure
serial creation and avoid concurrent name-normalization collisions.
🧹 Nitpick comments (3)
packages/loot-core/src/platform/client/connection/index-types.ts (1)

7-15: Keep the proxy signature aligned with the transport contract.

args? makes every server.* method callable with no payload, so the new proxy still lets calls like server.createPayee() type-check. This surface is supposed to be the typed replacement for send(...), so I'd preserve the handler parameter list exactly and make the proxy explicitly promise-returning.

♻️ Proposed type shape
 export type ServerProxy = {
-  [K in keyof Handlers]: (
-    args?: Parameters<Handlers[K]>[0],
-  ) => ReturnType<Handlers[K]>;
+  [K in keyof Handlers]: (
+    ...args: Parameters<Handlers[K]>
+  ) => Promise<Awaited<ReturnType<Handlers[K]>>>;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/loot-core/src/platform/client/connection/index-types.ts` around
lines 7 - 15, The proxy type ServerProxy currently makes the handler argument
optional and doesn't force a Promise result; update ServerProxy so each method
preserves the handler parameter list exactly (use Parameters<Handlers[K]> to
keep the same parameter tuple instead of a single optional args) and make the
return type explicitly promise-returning (wrap/await the handler's ReturnType so
server.* methods return Promise of the handler result). Apply the same corrected
signature to the declared server and the init() return type so server and init()
align with the transport contract.
packages/loot-core/src/mocks/budget.ts (2)

746-747: Using console.error in loot-core.

As per coding guidelines, loot-core should use logger instead of console. However, this is mock/test code, so this may be acceptable.

Based on learnings: "Custom ESLint rule actual/prefer-logger-over-console enforces using logger instead of console in packages/loot-core/"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/loot-core/src/mocks/budget.ts` around lines 746 - 747, Replace the
direct console.error call with the project's logger (e.g., logger.error or
getLogger().error) so the mock uses the centralized logging API; specifically,
in the block that currently does console.error('Unknown account name for test
budget: ', account.name) (near the fillChecking(app, account, payees, allGroups)
call), import or obtain the logger instance at the top of the file and call
logger.error with the same message and account.name (or interpolate) to satisfy
the actual/prefer-logger-over-console rule while keeping behavior unchanged.

300-303: Consider using direct handler calls instead of runHandler for consistency.

The helper functions use app.runHandler('transactions-batch-update', ...) while other places use app['handler-name'](). If transactions-batch-update handler exists on the type, you could use app['transactions-batch-update']() for consistency.

However, if this is intentional for the incremental migration (keeping kebab-case handlers working via runHandler), this is acceptable.

Also applies to: 336-339, 381-384, 418-421, 456-459

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/loot-core/src/mocks/budget.ts` around lines 300 - 303, Replace uses
of app.runHandler('transactions-batch-update', {...}) with the direct handler
call style app['transactions-batch-update']({...}) for consistency with other
helpers; locate occurrences of app.runHandler('transactions-batch-update', ...)
(including the similar calls later in the file) and change them to the bracketed
handler invocation (app['transactions-batch-update'](...)) only if that handler
exists on the app type, otherwise leave runHandler if this kebab-case
indirection is intentionally required for incremental migration.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/loot-core/src/platform/client/connection/index.ts`:
- Around line 102-107: init() currently caches the connection promise in
initPromise but clearServer() doesn't reset it, so calling clearServer() leaves
initPromise pointing to a stale/closed socket; update clearServer() to set
initPromise = undefined (or null) after closing/clearing the socket and any
related state so that the next call to init() will call new
Promise(connectSocket) again, referencing the symbols init, clearServer,
initPromise, and connectSocket to locate and modify the code.

In `@packages/loot-core/src/server/api.ts`:
- Line 86: handlers is initialized empty but api handler closures call delegated
methods like 'load-budget', 'get-budgets', 'close-budget' that are never
assigned, causing runtime undefined calls; fix by wiring the non-API delegate
methods into the local handlers object before calling createApp()—for example,
import or obtain the BudgetFileHandlers (the object that implements
'load-budget', 'get-budgets', 'download-budget', etc.) and merge its methods
into the local handlers (e.g., Object.assign(handlers, budgetFileHandlers)) or
otherwise assign each delegate function to handlers['load-budget'],
handlers['get-budgets'], handlers['close-budget'], etc., ensuring types still
satisfy ApiHandlers, or alternatively refactor the api handler closures to call
the combined app/mainApp that already registers those delegate methods instead
of referencing the local handlers bag.

---

Outside diff comments:
In `@packages/loot-core/src/server/importers/ynab5.ts`:
- Around line 451-461: importPayees currently fans out api/payee-create with
Promise.all causing duplicate-payee races; change it to process payees
sequentially by iterating (e.g., for...of) over data.payees, skipping deleted,
awaiting each mainApp['api/payee-create'] call before proceeding, and setting
entityIdMap.set(payee.id, id) immediately after each awaited call to ensure
serial creation and avoid concurrent name-normalization collisions.

---

Nitpick comments:
In `@packages/loot-core/src/mocks/budget.ts`:
- Around line 746-747: Replace the direct console.error call with the project's
logger (e.g., logger.error or getLogger().error) so the mock uses the
centralized logging API; specifically, in the block that currently does
console.error('Unknown account name for test budget: ', account.name) (near the
fillChecking(app, account, payees, allGroups) call), import or obtain the logger
instance at the top of the file and call logger.error with the same message and
account.name (or interpolate) to satisfy the actual/prefer-logger-over-console
rule while keeping behavior unchanged.
- Around line 300-303: Replace uses of
app.runHandler('transactions-batch-update', {...}) with the direct handler call
style app['transactions-batch-update']({...}) for consistency with other
helpers; locate occurrences of app.runHandler('transactions-batch-update', ...)
(including the similar calls later in the file) and change them to the bracketed
handler invocation (app['transactions-batch-update'](...)) only if that handler
exists on the app type, otherwise leave runHandler if this kebab-case
indirection is intentionally required for incremental migration.

In `@packages/loot-core/src/platform/client/connection/index-types.ts`:
- Around line 7-15: The proxy type ServerProxy currently makes the handler
argument optional and doesn't force a Promise result; update ServerProxy so each
method preserves the handler parameter list exactly (use Parameters<Handlers[K]>
to keep the same parameter tuple instead of a single optional args) and make the
return type explicitly promise-returning (wrap/await the handler's ReturnType so
server.* methods return Promise of the handler result). Apply the same corrected
signature to the declared server and the init() return type so server and init()
align with the transport contract.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8b9b88cb-e914-4c87-85cf-d3afcbbded00

📥 Commits

Reviewing files that changed from the base of the PR and between a8a2d23 and ae4e1f9.

📒 Files selected for processing (33)
  • packages/desktop-client/src/components/ManageRules.tsx
  • packages/desktop-client/src/components/mobile/payees/MobilePayeeEditPage.tsx
  • packages/desktop-client/src/components/mobile/payees/MobilePayeesPage.tsx
  • packages/desktop-client/src/components/modals/MergeUnusedPayeesModal.tsx
  • packages/desktop-client/src/components/payees/ManagePayeesWithData.tsx
  • packages/desktop-client/src/payees/location-adapters.ts
  • packages/desktop-client/src/payees/mutations.ts
  • packages/desktop-client/src/payees/queries.ts
  • packages/loot-core/src/mocks/budget.ts
  • packages/loot-core/src/platform/client/connection/index-types.ts
  • packages/loot-core/src/platform/client/connection/index.browser.ts
  • packages/loot-core/src/platform/client/connection/index.ts
  • packages/loot-core/src/platform/server/connection/index-types.ts
  • packages/loot-core/src/platform/server/connection/index.electron.ts
  • packages/loot-core/src/platform/server/connection/index.ts
  • packages/loot-core/src/server/accounts/app.ts
  • packages/loot-core/src/server/api.ts
  • packages/loot-core/src/server/app.ts
  • packages/loot-core/src/server/budgetfiles/app.ts
  • packages/loot-core/src/server/importers/actual.ts
  • packages/loot-core/src/server/importers/index.ts
  • packages/loot-core/src/server/importers/ynab4.ts
  • packages/loot-core/src/server/importers/ynab5.ts
  • packages/loot-core/src/server/main-app.ts
  • packages/loot-core/src/server/main.test.ts
  • packages/loot-core/src/server/main.ts
  • packages/loot-core/src/server/mutators.ts
  • packages/loot-core/src/server/payees/app.ts
  • packages/loot-core/src/server/sync/index.ts
  • packages/loot-core/src/server/util/budget-name.ts
  • packages/loot-core/src/types/app.ts
  • packages/loot-core/src/types/handlers.ts
  • packages/loot-core/src/types/server-handlers.ts
💤 Files with no reviewable changes (2)
  • packages/loot-core/src/server/main-app.ts
  • packages/loot-core/src/types/server-handlers.ts

Comment on lines +102 to 107
export const init: T.Init = function () {
if (!initPromise) {
initPromise = new Promise(connectSocket);
}
return initPromise;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

init() is now idempotent, but clearServer() doesn't reset initPromise.

The caching pattern is good, but if clearServer() is called (lines 171-175), a subsequent init() will return the stale promise pointing to the closed socket. Consider resetting initPromise in clearServer().

Proposed fix
 export const clearServer: T.ClearServer = async function () {
   if (socketClient != null) {
-    return new Promise(closeSocket);
+    await new Promise(closeSocket);
+    initPromise = null;
   }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/loot-core/src/platform/client/connection/index.ts` around lines 102
- 107, init() currently caches the connection promise in initPromise but
clearServer() doesn't reset it, so calling clearServer() leaves initPromise
pointing to a stale/closed socket; update clearServer() to set initPromise =
undefined (or null) after closing/clearing the socket and any related state so
that the next call to init() will call new Promise(connectSocket) again,
referencing the symbols init, clearServer, initPromise, and connectSocket to
locate and modify the code.

@github-actions
Copy link
Contributor

🤖 Auto-generated Release Notes

Hey @joel-jeremy! I've automatically created a release notes file based on CodeRabbit's analysis:

Category: Enhancements
Summary: Refactor client-server communication to use typed server proxy methods for improved clarity and safety.
File: upcoming-release-notes/7246.md

If you're happy with this release note, you can add it to your pull request. If not, you'll need to add your own before a maintainer can review your change.

@netlify
Copy link

netlify bot commented Mar 20, 2026

Deploy Preview for actualbudget-website ready!

Name Link
🔨 Latest commit 1598b6a
🔍 Latest deploy log https://app.netlify.com/projects/actualbudget-website/deploys/69bdd4a4915dee00089e3b92
😎 Deploy Preview https://deploy-preview-7246.www.actualbudget.org
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai coderabbitai bot removed the API Issues with the @actual-app/api package label Mar 20, 2026
@github-actions

This comment has been minimized.

@github-actions
Copy link
Contributor

VRT tests ❌ failed. View the test report.

To update the VRT screenshots, comment /update-vrt on this PR. The VRT update operation takes about 50 minutes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/loot-core/src/server/api.ts (1)

709-741: Inconsistent handler invocation style during migration.

Some handlers use dot notation with camelCase method names:

  • mainApp.getCommonPayees()
  • mainApp.getPayees()
  • mainApp.createPayee()
  • mainApp.batchChangePayees()
  • mainApp.mergePayees()

While others use bracket notation with kebab-case:

  • mainApp['rules-get']()
  • mainApp['budget/budget-amount']()

This inconsistency is expected given the PR description mentions incremental migration to camelCase. Consider adding a comment noting this is transitional, or track which handlers still need renaming.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/loot-core/src/server/api.ts` around lines 709 - 741, Add a short
transitional comment above this handlers block stating that handler invocation
styles are being migrated from bracket/kebab-case to dot/camelCase and that
mixed usage is intentional for now; reference the specific handlers to clarify
scope (e.g., handlers['api/payees-get'], handlers['api/payee-create'],
handlers['api/payees-merge'] and the still-kebab callers like
mainApp['rules-get'] and mainApp['budget/budget-amount']), and add a TODO with a
tracking ticket or label indicating remaining functions to rename (e.g., list of
kebab-case handlers) so future PRs can complete the migration.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/loot-core/src/server/api.ts`:
- Line 1027: The test still imports and calls the removed installAPI; update it
to import the new app export instead (import { app } from './api') and use app
directly in assertions (e.g., call the request/handler entry points on app or
reference app.handlers if the test expects the handlers object). If the test
needs to construct the app with a custom ServerHandlers, import and call
createApp(ServerHandlers) instead (use the createApp symbol) or adjust
expectations to use the already-created app; replace any use of installAPI(...)
with one of these approaches.

---

Nitpick comments:
In `@packages/loot-core/src/server/api.ts`:
- Around line 709-741: Add a short transitional comment above this handlers
block stating that handler invocation styles are being migrated from
bracket/kebab-case to dot/camelCase and that mixed usage is intentional for now;
reference the specific handlers to clarify scope (e.g.,
handlers['api/payees-get'], handlers['api/payee-create'],
handlers['api/payees-merge'] and the still-kebab callers like
mainApp['rules-get'] and mainApp['budget/budget-amount']), and add a TODO with a
tracking ticket or label indicating remaining functions to rename (e.g., list of
kebab-case handlers) so future PRs can complete the migration.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9fe4e024-acbf-44d3-9062-f05e42750a0c

📥 Commits

Reviewing files that changed from the base of the PR and between 54039b3 and 07ace07.

📒 Files selected for processing (4)
  • packages/loot-core/src/server/api.ts
  • packages/loot-core/src/server/main.ts
  • packages/loot-core/src/types/handlers.ts
  • upcoming-release-notes/7246.md
✅ Files skipped from review due to trivial changes (1)
  • upcoming-release-notes/7246.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/loot-core/src/types/handlers.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/loot-core/src/types/handlers.ts (1)

45-45: Consider removing redundant {} & prefix.

Intersecting with an empty object type ({}) has no effect on the resulting type. This appears to be unnecessary.

♻️ Proposed simplification
-export type Handlers = {} & ServerHandlers & ApiHandlers;
+export type Handlers = ServerHandlers & ApiHandlers;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/loot-core/src/types/handlers.ts` at line 45, The type alias Handlers
currently intersects with an empty object ("export type Handlers = {} &
ServerHandlers & ApiHandlers;"); remove the redundant "{} &" so the declaration
becomes a direct intersection of ServerHandlers and ApiHandlers (i.e., change
Handlers to use ServerHandlers & ApiHandlers) to simplify the type without
changing behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/loot-core/src/server/api.test.ts`:
- Around line 21-27: The test's mock of apiApp['accounts-bank-sync'] won't
intercept the internal call from the api/bank-sync handler because that handler
uses a different app instance; update the test to mock the exact dependency the
handler calls (or provide the same app instance to the handler). Concretely,
either (a) ensure the handler under test receives the same apiApp object you set
the mock on (pass that instance into the api/bank-sync handler or factory), or
(b) vi.mock or stub the module/function the handler invokes for account sync
(the real implementation called by "api/bank-sync") so your mock of
"accounts-bank-sync" is the one actually executed; adjust the test to call the
handler with the injected instance or mocked module so the rejection path
returns { errors: ['connection-failed'] } and the expect(...).rejects.toThrow
assertion runs against that behavior.
- Around line 12-17: The test currently mocks apiApp['accounts-bank-sync'] which
is never called for single-account sync; instead mock mainApp['bank-sync'] (the
handler invoked by api/bank-sync when accountId is provided) by replacing the
apiApp mock with a vi.fn().mockResolvedValue({ errors: [] }) on
mainApp['bank-sync'], call apiApp['api/bank-sync']({ accountId: 'account1' }),
and assert that mainApp['bank-sync'] was called with the expected single-account
argument; alternatively, refactor tests to inject or share the same app instance
from createApp so handler mocks on apiApp affect calls inside the handler.

---

Nitpick comments:
In `@packages/loot-core/src/types/handlers.ts`:
- Line 45: The type alias Handlers currently intersects with an empty object
("export type Handlers = {} & ServerHandlers & ApiHandlers;"); remove the
redundant "{} &" so the declaration becomes a direct intersection of
ServerHandlers and ApiHandlers (i.e., change Handlers to use ServerHandlers &
ApiHandlers) to simplify the type without changing behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6eb4799f-14ac-4871-bd8c-3f76b1898cef

📥 Commits

Reviewing files that changed from the base of the PR and between 07ace07 and 573238a.

📒 Files selected for processing (3)
  • packages/loot-core/src/server/api.test.ts
  • packages/loot-core/src/server/main.ts
  • packages/loot-core/src/types/handlers.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/loot-core/src/server/main.ts

@MatissJanis
Copy link
Member

Really cool. WDYT about starting to use the API directly already? I think it should be possible with this PR: #7247

(spoilers: I have not properly tested though and won't have time in the next week.. but theoretically it should work).

@joel-jeremy
Copy link
Member Author

Really cool. WDYT about starting to use the API directly already? I think it should be possible with this PR: #7247

(spoilers: I have not properly tested though and won't have time in the next week.. but theoretically it should work).

That would be nice! I could adopt this PR and remove the desktop-client changes since we're going to go through api. Once we merge your PR, I can make a follow PR to update api to use the server proxy added on this PR and remove the send calls (possibly remove the manual translation on the methods.ts file)

@coderabbitai coderabbitai bot added the API Issues with the @actual-app/api package label Mar 20, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/loot-core/src/server/api.test.ts`:
- Around line 26-36: The test contains an if without braces inside the mocked
implementation for mainApp.runHandler; update the anonymous async function in
the vi.spyOn(...).mockImplementation to wrap the if body in curly braces (i.e.,
add { return { errors: [...] }; } for the branch where name ===
'accounts-bank-sync') so the code conforms to the ESLint `curly` rule while
leaving the thrown Error for unexpected handlers and the apiApp['api/bank-sync']
assertion unchanged.
- Around line 12-22: The test currently creates a spy on mainApp.runHandler
inline and uses it directly in expect, and also has an if without braces in the
mockImplementation; fix by assigning the spy to a local variable (e.g., const
runHandlerSpy = vi.spyOn(mainApp, 'runHandler')...) and use that variable in the
assertion to avoid the unbound-method lint error, and add curly braces around
the if body inside the mockImplementation for mainApp.runHandler to satisfy the
missing-braces rule; keep the mocked behavior (returning { errors: [] } for name
=== 'accounts-bank-sync' and throwing for others) and the call to
apiApp['api/bank-sync']({ accountId: 'account1' }) unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ca8f8091-f25c-44b6-8599-c2b8f1b74bd0

📥 Commits

Reviewing files that changed from the base of the PR and between 573238a and 1598b6a.

📒 Files selected for processing (3)
  • packages/loot-core/src/server/accounts/app-bank-sync.test.ts
  • packages/loot-core/src/server/api.test.ts
  • packages/loot-core/src/server/api.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/loot-core/src/server/api.ts

Comment on lines 12 to 22
vi.spyOn(mainApp, 'runHandler').mockImplementation(
async (name: string) => {
if (name === 'accounts-bank-sync') return { errors: [] };
throw new Error(`Unexpected handler: ${name}`);
},
);

await handlers['api/bank-sync']({ accountId: 'account1' });
expect(handlers['accounts-bank-sync']).toHaveBeenCalledWith({
await apiApp['api/bank-sync']({ accountId: 'account1' });
expect(mainApp.runHandler).toHaveBeenCalledWith('accounts-bank-sync', {
ids: ['account1'],
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix lint violations: unbound-method and missing curly braces.

Static analysis flags two issues:

  1. Line 20: unbound-method - referencing mainApp.runHandler in the assertion can cause scoping issues
  2. Line 14: Missing curly braces after if condition

Store the spy in a variable to fix the unbound-method issue and add braces to the if statement.

🔧 Proposed fix
     it('should sync a single account when accountId is provided', async () => {
-      vi.spyOn(mainApp, 'runHandler').mockImplementation(
+      const runHandlerSpy = vi.spyOn(mainApp, 'runHandler').mockImplementation(
         async (name: string) => {
-          if (name === 'accounts-bank-sync') return { errors: [] };
+          if (name === 'accounts-bank-sync') {
+            return { errors: [] };
+          }
           throw new Error(`Unexpected handler: ${name}`);
         },
       );

       await apiApp['api/bank-sync']({ accountId: 'account1' });
-      expect(mainApp.runHandler).toHaveBeenCalledWith('accounts-bank-sync', {
+      expect(runHandlerSpy).toHaveBeenCalledWith('accounts-bank-sync', {
         ids: ['account1'],
       });
     });
🧰 Tools
🪛 GitHub Check: autofix

[failure] 20-20: typescript-eslint(unbound-method)
Avoid referencing unbound methods which may cause unintentional scoping of this.

🪛 GitHub Check: lint

[failure] 20-20: typescript-eslint(unbound-method)
Avoid referencing unbound methods which may cause unintentional scoping of this.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/loot-core/src/server/api.test.ts` around lines 12 - 22, The test
currently creates a spy on mainApp.runHandler inline and uses it directly in
expect, and also has an if without braces in the mockImplementation; fix by
assigning the spy to a local variable (e.g., const runHandlerSpy =
vi.spyOn(mainApp, 'runHandler')...) and use that variable in the assertion to
avoid the unbound-method lint error, and add curly braces around the if body
inside the mockImplementation for mainApp.runHandler to satisfy the
missing-braces rule; keep the mocked behavior (returning { errors: [] } for name
=== 'accounts-bank-sync' and throwing for others) and the call to
apiApp['api/bank-sync']({ accountId: 'account1' }) unchanged.

Comment on lines +26 to +36
vi.spyOn(mainApp, 'runHandler').mockImplementation(
async (name: string) => {
if (name === 'accounts-bank-sync')
return { errors: [{ message: 'connection-failed' }] };
throw new Error(`Unexpected handler: ${name}`);
},
);

await expect(
handlers['api/bank-sync']({ accountId: 'account2' }),
).rejects.toThrow('Bank sync error: connection-failed');

expect(getBankSyncError).toHaveBeenCalledWith('connection-failed');
apiApp['api/bank-sync']({ accountId: 'account2' }),
).rejects.toThrow('connection-failed');
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Same lint violation: missing curly braces after if condition.

Line 29 violates the ESLint curly rule. Add braces around the if body.

🔧 Proposed fix
     it('should throw an error when bank sync fails', async () => {
       vi.spyOn(mainApp, 'runHandler').mockImplementation(
         async (name: string) => {
-          if (name === 'accounts-bank-sync')
-            return { errors: [{ message: 'connection-failed' }] };
+          if (name === 'accounts-bank-sync') {
+            return { errors: [{ message: 'connection-failed' }] };
+          }
           throw new Error(`Unexpected handler: ${name}`);
         },
       );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
vi.spyOn(mainApp, 'runHandler').mockImplementation(
async (name: string) => {
if (name === 'accounts-bank-sync')
return { errors: [{ message: 'connection-failed' }] };
throw new Error(`Unexpected handler: ${name}`);
},
);
await expect(
handlers['api/bank-sync']({ accountId: 'account2' }),
).rejects.toThrow('Bank sync error: connection-failed');
expect(getBankSyncError).toHaveBeenCalledWith('connection-failed');
apiApp['api/bank-sync']({ accountId: 'account2' }),
).rejects.toThrow('connection-failed');
vi.spyOn(mainApp, 'runHandler').mockImplementation(
async (name: string) => {
if (name === 'accounts-bank-sync') {
return { errors: [{ message: 'connection-failed' }] };
}
throw new Error(`Unexpected handler: ${name}`);
},
);
await expect(
apiApp['api/bank-sync']({ accountId: 'account2' }),
).rejects.toThrow('connection-failed');
🧰 Tools
🪛 GitHub Check: lint

[failure] 29-29: eslint(curly)
Expected { after 'if' condition.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/loot-core/src/server/api.test.ts` around lines 26 - 36, The test
contains an if without braces inside the mocked implementation for
mainApp.runHandler; update the anonymous async function in the
vi.spyOn(...).mockImplementation to wrap the if body in curly braces (i.e., add
{ return { errors: [...] }; } for the branch where name ===
'accounts-bank-sync') so the code conforms to the ESLint `curly` rule while
leaving the thrown Error for unexpected handlers and the apiApp['api/bank-sync']
assertion unchanged.

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

Labels

API Issues with the @actual-app/api package 🚧 WIP

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants