diff --git a/packages/desktop-client/src/browser-preload.browser.js b/packages/desktop-client/src/browser-preload.browser.js index 3c29ddabf1..5dc18eda89 100644 --- a/packages/desktop-client/src/browser-preload.browser.js +++ b/packages/desktop-client/src/browser-preload.browser.js @@ -86,6 +86,12 @@ global.Actual = { }); }, + startSyncServer: () => {}, + + stopSyncServer: () => {}, + + isSyncServerRunning: () => false, + startOAuthServer: () => { return ''; }, diff --git a/packages/desktop-electron/index.ts b/packages/desktop-electron/index.ts index d1a8e15c45..65b8baf232 100644 --- a/packages/desktop-electron/index.ts +++ b/packages/desktop-electron/index.ts @@ -65,7 +65,7 @@ if (isPlaywrightTest) { // be closed automatically when the JavaScript object is garbage collected. let clientWin: BrowserWindow | null; let serverProcess: UtilityProcess | null; -let actualServerProcess: UtilityProcess | null; +let syncServerProcess: UtilityProcess | null; let oAuthServer: ReturnType | null; @@ -264,19 +264,19 @@ async function startSyncServer() { let syncServerStarted = false; const syncServerPromise = new Promise(resolve => { - actualServerProcess = utilityProcess.fork(serverPath, [], forkOptions); + syncServerProcess = utilityProcess.fork(serverPath, [], forkOptions); - actualServerProcess.stdout?.on('data', (chunk: Buffer) => { + syncServerProcess.stdout?.on('data', (chunk: Buffer) => { // Send the Server console.log messages to the main browser window logMessage('info', `Sync-Server: ${chunk.toString('utf8')}`); }); - actualServerProcess.stderr?.on('data', (chunk: Buffer) => { + syncServerProcess.stderr?.on('data', (chunk: Buffer) => { // Send the Server console.error messages out to the main browser window logMessage('error', `Sync-Server: ${chunk.toString('utf8')}`); }); - actualServerProcess.on('message', msg => { + syncServerProcess.on('message', msg => { switch (msg.type) { case 'server-started': logMessage('info', 'Sync-Server: Actual Sync Server has started!'); @@ -310,6 +310,12 @@ async function startSyncServer() { } } +async function stopSyncServer() { + syncServerProcess?.kill(); + syncServerProcess = null; + logMessage('info', 'Sync-Server: Stopped'); +} + async function createWindow() { const windowState = await getWindowState(); @@ -548,6 +554,14 @@ ipcMain.on('get-bootstrap-data', event => { event.returnValue = payload; }); +ipcMain.handle('start-sync-server', async () => startSyncServer()); + +ipcMain.handle('stop-sync-server', async () => stopSyncServer()); + +ipcMain.handle('is-sync-server-running', async () => + syncServerProcess ? true : false, +); + ipcMain.handle('start-oauth-server', async () => { const { url, server: newServer } = await createOAuthServer(); oAuthServer = newServer; diff --git a/packages/desktop-electron/preload.ts b/packages/desktop-electron/preload.ts index 63f491133d..2381b6cad2 100644 --- a/packages/desktop-electron/preload.ts +++ b/packages/desktop-electron/preload.ts @@ -29,6 +29,12 @@ contextBridge.exposeInMainWorld('Actual', { }); }, + startSyncServer: () => ipcRenderer.invoke('start-sync-server'), + + stopSyncServer: () => ipcRenderer.invoke('stop-sync-server'), + + isSyncServerRunning: () => ipcRenderer.invoke('is-sync-server-running'), + startOAuthServer: () => ipcRenderer.invoke('start-oauth-server'), relaunch: () => { 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 be45225229..d094497b9e 100644 --- a/packages/loot-core/src/platform/server/asyncStorage/__mocks__/index.ts +++ b/packages/loot-core/src/platform/server/asyncStorage/__mocks__/index.ts @@ -20,14 +20,19 @@ export const removeItem: T.RemoveItem = async function (key) { export async function multiGet( 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]]] }, - ); - }); +): Promise<{ [P in K[number]]: GlobalPrefsJson[P] }> { + const results = keys.map(key => [key, store[key]]) as { + [P in keyof K]: [K[P], GlobalPrefsJson[K[P]]]; + }; + + // Convert the array of tuples to an object with properly typed properties + return results.reduce( + (acc, [key, value]) => { + acc[key] = value; + return acc; + }, + {} as { [P in K[number]]: GlobalPrefsJson[P] }, + ); } export const multiSet: T.MultiSet = async function (keyValues) { 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 39fb728125..5bace987dc 100644 --- a/packages/loot-core/src/platform/server/asyncStorage/index.d.ts +++ b/packages/loot-core/src/platform/server/asyncStorage/index.d.ts @@ -21,7 +21,8 @@ export type RemoveItem = typeof removeItem; export function multiGet( keys: K, -): Promise<{ [P in keyof K]: [K[P], GlobalPrefsJson[K[P]]] }>; +): Promise<{ [P in K[number]]: GlobalPrefsJson[P] }>; + export type MultiGet = typeof multiGet; export function multiSet( 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 0664d7dc8e..ddd3985606 100644 --- a/packages/loot-core/src/platform/server/asyncStorage/index.electron.ts +++ b/packages/loot-core/src/platform/server/asyncStorage/index.electron.ts @@ -58,10 +58,19 @@ export const removeItem: T.RemoveItem = function (key) { export async function multiGet( keys: K, -) { - return keys.map(key => [key, store[key]]) as { +): Promise<{ [P in K[number]]: GlobalPrefsJson[P] }> { + const results = keys.map(key => [key, store[key]]) as { [P in keyof K]: [K[P], GlobalPrefsJson[K[P]]]; }; + + // Convert the array of tuples to an object with properly typed properties + return results.reduce( + (acc, [key, value]) => { + acc[key] = value; + return acc; + }, + {} as { [P in K[number]]: GlobalPrefsJson[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 c504f66303..ae4ff70bc9 100644 --- a/packages/loot-core/src/platform/server/asyncStorage/index.ts +++ b/packages/loot-core/src/platform/server/asyncStorage/index.ts @@ -50,25 +50,37 @@ export const removeItem: T.RemoveItem = async function (key) { export async function multiGet( keys: K, -) { +): Promise<{ [P in K[number]]: GlobalPrefsJson[P] }> { const db = await getDatabase(); const transaction = db.transaction(['asyncStorage'], 'readonly'); const objectStore = transaction.objectStore('asyncStorage'); - const promise = Promise.all( + const results = await Promise.all( keys.map(key => { - return new Promise<[string, string]>((resolve, reject) => { - const req = objectStore.get(key); - req.onerror = e => reject(e); - // @ts-expect-error fix me - req.onsuccess = e => resolve([key, e.target.result]); - }); + return new Promise<[K[number], GlobalPrefsJson[K[number]]]>( + (resolve, reject) => { + const req = objectStore.get(key); + req.onerror = e => reject(e); + req.onsuccess = e => { + const target = e.target as IDBRequest; + resolve([key, target.result]); + }; + }, + ); }), ); transaction.commit(); - return promise; + + // Convert the array of tuples to an object with properly typed properties + return results.reduce( + (acc, [key, value]) => { + acc[key] = value; + return acc; + }, + {} as { [P in K[number]]: GlobalPrefsJson[P] }, + ); } export const multiSet: T.MultiSet = async function (keyValues) { diff --git a/packages/loot-core/src/server/accounts/app.ts b/packages/loot-core/src/server/accounts/app.ts index 44350efe87..f9943eadc8 100644 --- a/packages/loot-core/src/server/accounts/app.ts +++ b/packages/loot-core/src/server/accounts/app.ts @@ -883,10 +883,8 @@ async function accountsBankSync({ }: { ids: Array; }): Promise { - const [[, userId], [, userKey]] = await asyncStorage.multiGet([ - 'user-id', - 'user-key', - ]); + const { 'user-id': userId, 'user-key': userKey } = + await asyncStorage.multiGet(['user-id', 'user-key']); const accounts = await db.runQuery< db.DbAccount & { bankId: db.DbBank['bank_id'] } diff --git a/packages/loot-core/src/server/preferences/app.ts b/packages/loot-core/src/server/preferences/app.ts index 01554a96b3..3cf0cc83cb 100644 --- a/packages/loot-core/src/server/preferences/app.ts +++ b/packages/loot-core/src/server/preferences/app.ts @@ -99,21 +99,25 @@ async function saveGlobalPrefs(prefs: GlobalPrefs) { prefs.serverSelfSignedCert, ); } + if (prefs.syncServerConfig !== undefined) { + await asyncStorage.setItem('syncServerConfig', prefs.syncServerConfig); + } return 'ok'; } async function loadGlobalPrefs(): Promise { - const [ - [, floatingSidebar], - [, categoryExpandedState], - [, maxMonths], - [, documentDir], - [, encryptKey], - [, language], - [, theme], - [, preferredDarkTheme], - [, serverSelfSignedCert], - ] = await asyncStorage.multiGet([ + const { + 'floating-sidebar': floatingSidebar, + 'category-expanded-state': categoryExpandedState, + 'max-months': maxMonths, + 'document-dir': documentDir, + 'encrypt-key': encryptKey, + language, + theme, + 'preferred-dark-theme': preferredDarkTheme, + 'server-self-signed-cert': serverSelfSignedCert, + syncServerConfig, + } = await asyncStorage.multiGet([ 'floating-sidebar', 'category-expanded-state', 'max-months', @@ -123,6 +127,7 @@ async function loadGlobalPrefs(): Promise { 'theme', 'preferred-dark-theme', 'server-self-signed-cert', + 'syncServerConfig', ] as const); return { floatingSidebar: floatingSidebar === 'true', @@ -144,6 +149,7 @@ async function loadGlobalPrefs(): Promise { ? preferredDarkTheme : 'dark', serverSelfSignedCert: serverSelfSignedCert || undefined, + syncServerConfig: syncServerConfig || undefined, }; } diff --git a/packages/loot-core/typings/window.d.ts b/packages/loot-core/typings/window.d.ts index 53ed497e04..10fffd582f 100644 --- a/packages/loot-core/typings/window.d.ts +++ b/packages/loot-core/typings/window.d.ts @@ -29,6 +29,9 @@ type Actual = { ) => void; isUpdateReadyForDownload: () => boolean; waitForUpdateReadyForDownload: () => Promise; + startSyncServer: () => Promise; + stopSyncServer: () => Promise; + isSyncServerRunning: () => Promise; startOAuthServer: () => Promise; }; diff --git a/upcoming-release-notes/4847.md b/upcoming-release-notes/4847.md new file mode 100644 index 0000000000..b7f99c2a29 --- /dev/null +++ b/upcoming-release-notes/4847.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [MikesGlitch] +--- + +Allowing the UI to call internal sync server commands when running in the Desktop app