diff --git a/.gitattributes b/.gitattributes
index 21f3d101a30..a592bc40b1e 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -16,4 +16,4 @@ yarn.lock text eol=lf
# Denote all files that are truly binary and should not be modified.
*.png binary
-*.jpg binary
\ No newline at end of file
+*.jpg binary
diff --git a/packages/desktop-client/src/browser-preload.browser.js b/packages/desktop-client/src/browser-preload.browser.js
index 803ed2ac5b9..37bd1f53719 100644
--- a/packages/desktop-client/src/browser-preload.browser.js
+++ b/packages/desktop-client/src/browser-preload.browser.js
@@ -125,6 +125,7 @@ global.Actual = {
applyAppUpdate: () => {},
updateAppMenu: isBudgetOpen => {},
+ ipcConnect: () => {},
getServerSocket: async () => {
return worker;
},
diff --git a/packages/desktop-client/src/components/accounts/Account.jsx b/packages/desktop-client/src/components/accounts/Account.jsx
index a91ad99ee55..e61c46c68ad 100644
--- a/packages/desktop-client/src/components/accounts/Account.jsx
+++ b/packages/desktop-client/src/components/accounts/Account.jsx
@@ -463,6 +463,7 @@ class AccountInternal extends PureComponent {
onImport = async () => {
const accountId = this.props.accountId;
const account = this.props.accounts.find(acct => acct.id === accountId);
+ const categories = await this.props.getCategories();
if (account) {
const res = await window.Actual.openFileDialog({
@@ -477,6 +478,7 @@ class AccountInternal extends PureComponent {
if (res) {
this.props.pushModal('import-transactions', {
accountId,
+ categories,
filename: res[0],
onImported: didChange => {
if (didChange) {
diff --git a/packages/desktop-client/src/components/budget/report/ReportComponents.tsx b/packages/desktop-client/src/components/budget/report/ReportComponents.tsx
index b50cb9171a0..b33faba456d 100644
--- a/packages/desktop-client/src/components/budget/report/ReportComponents.tsx
+++ b/packages/desktop-client/src/components/budget/report/ReportComponents.tsx
@@ -189,9 +189,11 @@ export const CategoryMonth = memo(function CategoryMonth({
);
diff --git a/packages/desktop-client/src/components/budget/report/budgetsummary/IncomeProgress.tsx b/packages/desktop-client/src/components/budget/report/budgetsummary/IncomeProgress.tsx
index 66b64abd50c..f264b07e9d8 100644
--- a/packages/desktop-client/src/components/budget/report/budgetsummary/IncomeProgress.tsx
+++ b/packages/desktop-client/src/components/budget/report/budgetsummary/IncomeProgress.tsx
@@ -28,7 +28,7 @@ export function IncomeProgress({ current, target }: IncomeProgressProps) {
);
diff --git a/packages/desktop-client/src/components/modals/ImportTransactions.jsx b/packages/desktop-client/src/components/modals/ImportTransactions.jsx
index 8c55e3b322e..79f23a37a84 100644
--- a/packages/desktop-client/src/components/modals/ImportTransactions.jsx
+++ b/packages/desktop-client/src/components/modals/ImportTransactions.jsx
@@ -194,14 +194,24 @@ function getInitialMappings(transactions) {
fields.find(([name, value]) => value.match(/^-?[.,\d]+$/)),
);
+ const categoryField = key(
+ fields.find(([name, value]) => name.toLowerCase().includes('category')),
+ );
+
const payeeField = key(
- fields.find(([name, value]) => name !== dateField && name !== amountField),
+ fields.find(
+ ([name, value]) =>
+ name !== dateField && name !== amountField && name !== categoryField,
+ ),
);
const notesField = key(
fields.find(
([name, value]) =>
- name !== dateField && name !== amountField && name !== payeeField,
+ name !== dateField &&
+ name !== amountField &&
+ name !== categoryField &&
+ name !== payeeField,
),
);
@@ -221,6 +231,7 @@ function getInitialMappings(transactions) {
payee: payeeField,
notes: notesField,
inOut: inOutField,
+ category: categoryField,
};
}
@@ -290,6 +301,19 @@ function parseAmountFields(
};
}
+function parseCategoryFields(trans, categories) {
+ let match = null;
+ categories.forEach(category => {
+ if (category.id === trans.category) {
+ return null;
+ }
+ if (category.name === trans.category) {
+ match = category.id;
+ }
+ });
+ return match;
+}
+
function Transaction({
transaction: rawTransaction,
fieldMappings,
@@ -301,7 +325,9 @@ function Transaction({
outValue,
flipAmount,
multiplierAmount,
+ categories,
}) {
+ const categoryList = categories.map(category => category.name);
const transaction = useMemo(
() =>
fieldMappings
@@ -348,6 +374,14 @@ function Transaction({
{transaction.notes}
+
+ {categoryList.includes(transaction.category) && transaction.category}
+
{splitMode ? (
<>
+
+
+ onChange('category', name)}
+ hasHeaderRow={hasHeaderRow}
+ firstTransaction={transactions[0]}
+ />
+
{splitMode ? (
<>
@@ -676,7 +721,7 @@ export function ImportTransactions({ modalProps, options }) {
const [outValue, setOutValue] = useState('');
const [flipAmount, setFlipAmount] = useState(false);
const [multiplierEnabled, setMultiplierEnabled] = useState(false);
- const { accountId, onImported } = options;
+ const { accountId, categories, onImported } = options;
// This cannot be set after parsing the file, because changing it
// requires re-parsing the file. This is different from the other
@@ -866,6 +911,11 @@ export function ImportTransactions({ modalProps, options }) {
break;
}
+ const category_id = parseCategoryFields(trans, categories.list);
+ if (category_id != null) {
+ trans.category = category_id;
+ }
+
const { inflow, outflow, inOut, ...finalTransaction } = trans;
finalTransactions.push({
...finalTransaction,
@@ -919,6 +969,7 @@ export function ImportTransactions({ modalProps, options }) {
{ name: 'Date', width: 200 },
{ name: 'Payee', width: 'flex' },
{ name: 'Notes', width: 'flex' },
+ { name: 'Category', width: 'flex' },
];
if (inOutMode) {
@@ -959,7 +1010,7 @@ export function ImportTransactions({ modalProps, options }) {
index}
renderEmpty={() => {
@@ -989,6 +1040,7 @@ export function ImportTransactions({ modalProps, options }) {
outValue={outValue}
flipAmount={flipAmount}
multiplierAmount={multiplierAmount}
+ categories={categories.list}
/>
)}
diff --git a/packages/desktop-client/src/components/reports/ChooseGraph.tsx b/packages/desktop-client/src/components/reports/ChooseGraph.tsx
index c7baea09b9b..c1da3e8b8a5 100644
--- a/packages/desktop-client/src/components/reports/ChooseGraph.tsx
+++ b/packages/desktop-client/src/components/reports/ChooseGraph.tsx
@@ -9,11 +9,11 @@ import { BarLineGraph } from './graphs/BarLineGraph';
import { DonutGraph } from './graphs/DonutGraph';
import { LineGraph } from './graphs/LineGraph';
import { StackedBarGraph } from './graphs/StackedBarGraph';
+import { ReportTable } from './graphs/tableGraph/ReportTable';
+import { ReportTableHeader } from './graphs/tableGraph/ReportTableHeader';
+import { ReportTableList } from './graphs/tableGraph/ReportTableList';
+import { ReportTableTotals } from './graphs/tableGraph/ReportTableTotals';
import { ReportOptions } from './ReportOptions';
-import { ReportTable } from './ReportTable';
-import { ReportTableHeader } from './ReportTableHeader';
-import { ReportTableList } from './ReportTableList';
-import { ReportTableTotals } from './ReportTableTotals';
type ChooseGraphProps = {
data: DataEntity;
diff --git a/packages/desktop-client/src/components/reports/ReportTable.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx
similarity index 92%
rename from packages/desktop-client/src/components/reports/ReportTable.tsx
rename to packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx
index 389d3e8be23..91a5ca1457a 100644
--- a/packages/desktop-client/src/components/reports/ReportTable.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx
@@ -6,8 +6,8 @@ import React, {
} from 'react';
import { type RefProp } from 'react-spring';
-import { type CSSProperties } from '../../style';
-import { View } from '../common/View';
+import { type CSSProperties } from '../../../../style';
+import { View } from '../../../common/View';
type ReportTableProps = {
saveScrollWidth?: (value: number) => void;
diff --git a/packages/desktop-client/src/components/reports/ReportTableHeader.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableHeader.tsx
similarity index 92%
rename from packages/desktop-client/src/components/reports/ReportTableHeader.tsx
rename to packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableHeader.tsx
index 57078fe32db..c95fb518f41 100644
--- a/packages/desktop-client/src/components/reports/ReportTableHeader.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableHeader.tsx
@@ -1,11 +1,10 @@
import React, { type UIEventHandler } from 'react';
import { type RefProp } from 'react-spring';
-import { styles, theme } from '../../style';
-import { View } from '../common/View';
-import { Row, Cell } from '../table';
-
-import { type MonthData } from './entities';
+import { styles, theme } from '../../../../style';
+import { View } from '../../../common/View';
+import { Row, Cell } from '../../../table';
+import { type MonthData } from '../../entities';
type ReportTableHeaderProps = {
scrollWidth?: number;
diff --git a/packages/desktop-client/src/components/reports/ReportTableList.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableList.tsx
similarity index 97%
rename from packages/desktop-client/src/components/reports/ReportTableList.tsx
rename to packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableList.tsx
index eeb6e6fc2e9..0cda809d6b4 100644
--- a/packages/desktop-client/src/components/reports/ReportTableList.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableList.tsx
@@ -6,9 +6,9 @@ import {
integerToCurrency,
} from 'loot-core/src/shared/util';
-import { type CSSProperties, styles, theme } from '../../style';
-import { View } from '../common/View';
-import { Row, Cell } from '../table';
+import { type CSSProperties, styles, theme } from '../../../../style';
+import { View } from '../../../common/View';
+import { Row, Cell } from '../../../table';
type TableRowProps = {
item: {
diff --git a/packages/desktop-client/src/components/reports/ReportTableTotals.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx
similarity index 95%
rename from packages/desktop-client/src/components/reports/ReportTableTotals.tsx
rename to packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx
index df0e5b019e3..be418de80c6 100644
--- a/packages/desktop-client/src/components/reports/ReportTableTotals.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx
@@ -7,11 +7,10 @@ import {
integerToCurrency,
} from 'loot-core/src/shared/util';
-import { styles, theme } from '../../style';
-import { View } from '../common/View';
-import { Row, Cell } from '../table';
-
-import { type DataEntity } from './entities';
+import { styles, theme } from '../../../../style';
+import { View } from '../../../common/View';
+import { Row, Cell } from '../../../table';
+import { type DataEntity } from '../../entities';
type ReportTableTotalsProps = {
data: DataEntity;
diff --git a/packages/desktop-electron/index.js b/packages/desktop-electron/index.js
index e585867c9e5..d8e03335217 100644
--- a/packages/desktop-electron/index.js
+++ b/packages/desktop-electron/index.js
@@ -5,11 +5,6 @@ const fs = require('fs');
require('module').globalPaths.push(__dirname + '/..');
-// Allow unsecure in dev
-if (isDev) {
- process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
-}
-
const {
app,
ipcMain,
@@ -18,6 +13,7 @@ const {
dialog,
shell,
protocol,
+ utilityProcess,
} = require('electron');
const promiseRetry = require('promise-retry');
@@ -30,15 +26,12 @@ protocol.registerSchemesAsPrivileged([
global.fetch = require('node-fetch');
const about = require('./about');
-const { getRandomPort } = require('get-port-please');
const getMenu = require('./menu');
const updater = require('./updater');
require('./security');
-const { fork } = require('child_process');
const path = require('path');
-const http = require('http');
require('./setRequireHook');
@@ -57,7 +50,6 @@ const WindowState = require('./window-state.js');
// be closed automatically when the JavaScript object is garbage collected.
let clientWin;
let serverProcess;
-let serverSocket;
updater.onEvent((type, data) => {
// Notify both the app and the about window
@@ -74,10 +66,10 @@ if (isDev) {
process.traceProcessWarnings = true;
}
-function createBackgroundProcess(socketName) {
- serverProcess = fork(
+function createBackgroundProcess() {
+ serverProcess = utilityProcess.fork(
__dirname + '/server.js',
- ['--subprocess', app.getVersion(), socketName],
+ ['--subprocess', app.getVersion()],
isDev ? { execArgv: ['--inspect'] } : undefined,
);
@@ -93,52 +85,20 @@ function createBackgroundProcess(socketName) {
updater.stop();
}
break;
+ case 'reply':
+ case 'error':
+ case 'push':
+ if (clientWin) {
+ clientWin.webContents.send('message', msg);
+ }
+ break;
default:
console.log('Unknown server message: ' + msg.type);
}
});
-
- return serverProcess;
-}
-
-const isPortFree = port =>
- new Promise(resolve => {
- const server = http
- .createServer()
- .listen(port, () => {
- server.close();
- resolve(true);
- })
- .on('error', () => {
- resolve(false);
- });
- });
-
-async function createSocketConnection() {
- if (!serverSocket) serverSocket = await getRandomPort();
-
- // Spawn the child process if it is not already running
- // (sometimes long child processes die, so we need to set them
- // up again)
- const isFree = await isPortFree(serverSocket);
- if (isFree) {
- await createBackgroundProcess(serverSocket);
- }
-
- if (!clientWin) {
- return;
- }
-
- // Send a heartbeat to the client whenever we attempt to create a new
- // sockets connection
- clientWin.webContents.executeJavaScript(
- `window.__actionsForMenu && window.__actionsForMenu.reconnect(${serverSocket})`,
- );
}
async function createWindow() {
- await createSocketConnection();
-
const windowState = await WindowState.get();
// Create the browser window.
@@ -194,10 +154,6 @@ async function createWindow() {
}
});
- win.webContents.on('did-finish-load', () => {
- win.webContents.send('set-socket', { name: serverSocket });
- });
-
// hit when middle-clicking buttons or with a target set to _blank
// always deny, optionally redirect to browser
win.webContents.setWindowOpenHandler(({ url }) => {
@@ -304,6 +260,8 @@ app.on('ready', async () => {
require('electron').powerMonitor.on('suspend', () => {
console.log('Suspending', new Date());
});
+
+ createBackgroundProcess();
});
app.on('window-all-closed', () => {
@@ -326,14 +284,6 @@ app.on('activate', () => {
}
});
-app.on('did-become-active', () => {
- // Reconnect whenever the window becomes active;
- // We don't know what might have happened in-between, so it's better
- // to be safe than sorry; the client can then decide if it wants to
- // reconnect or not.
- createSocketConnection();
-});
-
ipcMain.on('get-bootstrap-data', event => {
event.returnValue = {
version: app.getVersion(),
@@ -377,6 +327,14 @@ ipcMain.on('show-about', () => {
about.openAboutWindow();
});
+ipcMain.on('message', (_event, msg) => {
+ if (!serverProcess) {
+ return;
+ }
+
+ serverProcess.postMessage(msg.args);
+});
+
ipcMain.on('screenshot', () => {
if (isDev) {
const width = 1100;
diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json
index 22a1c928dc9..4f80a76bbc1 100644
--- a/packages/desktop-electron/package.json
+++ b/packages/desktop-electron/package.json
@@ -50,11 +50,9 @@
"electron-is-dev": "2.0.0",
"electron-log": "4.4.8",
"electron-updater": "6.1.7",
- "get-port-please": "3.0.1",
"loot-core": "*",
"node-fetch": "^2.6.9",
- "promise-retry": "^2.0.1",
- "ws": "8.13.0"
+ "promise-retry": "^2.0.1"
},
"devDependencies": {
"@electron/notarize": "2.2.0",
diff --git a/packages/desktop-electron/preload.js b/packages/desktop-electron/preload.js
index 8aac6545db5..2625c58a85b 100644
--- a/packages/desktop-electron/preload.js
+++ b/packages/desktop-electron/preload.js
@@ -3,15 +3,6 @@ const { ipcRenderer, contextBridge } = require('electron');
const { version: VERSION, isDev: IS_DEV } =
ipcRenderer.sendSync('get-bootstrap-data');
-let resolveSocketPromise;
-const socketPromise = new Promise(resolve => {
- resolveSocketPromise = resolve;
-});
-
-ipcRenderer.on('set-socket', (event, { name }) => {
- resolveSocketPromise(name);
-});
-
contextBridge.exposeInMainWorld('Actual', {
IS_DEV,
ACTUAL_VERSION: VERSION,
@@ -19,6 +10,17 @@ contextBridge.exposeInMainWorld('Actual', {
require('console').log(...args);
},
+ ipcConnect: func => {
+ func({
+ on(name, handler) {
+ return ipcRenderer.on(name, (_event, value) => handler(value));
+ },
+ emit(name, data) {
+ return ipcRenderer.send('message', { name, args: data });
+ },
+ });
+ },
+
relaunch: () => {
ipcRenderer.invoke('relaunch');
},
@@ -52,7 +54,7 @@ contextBridge.exposeInMainWorld('Actual', {
},
getServerSocket: () => {
- return socketPromise;
+ return null;
},
setTheme: theme => {
diff --git a/packages/desktop-electron/server.js b/packages/desktop-electron/server.js
index 109fb02336d..82cbacf80a6 100644
--- a/packages/desktop-electron/server.js
+++ b/packages/desktop-electron/server.js
@@ -9,22 +9,7 @@ function getBackend() {
return require('loot-core/lib-dist/bundle.desktop.js');
}
-if (process.argv[2] === '--subprocess') {
- const isDev = false;
- // let version = process.argv[3];
- const socketName = process.argv[4];
+const isDev = false;
- // Start the app
- getBackend().initApp(isDev, socketName);
-} else if (process.argv[2] === '--standalone') {
- require('source-map-support').install();
- getBackend().initApp(true, 'actual-standalone');
-} else {
- const { ipcRenderer } = require('electron');
- const isDev = true;
-
- ipcRenderer.on('set-socket', (event, { name }) => {
- // Start the app
- getBackend().initApp(isDev, name);
- });
-}
+// Start the app
+getBackend().initApp(isDev);
diff --git a/packages/loot-core/package.json b/packages/loot-core/package.json
index 7a12f944356..36d1d680e3f 100644
--- a/packages/loot-core/package.json
+++ b/packages/loot-core/package.json
@@ -37,8 +37,7 @@
"path-browserify": "^1.0.1",
"process": "^0.11.10",
"reselect": "^4.1.8",
- "stream-browserify": "^3.0.0",
- "ws": "8.13.0"
+ "stream-browserify": "^3.0.0"
},
"devDependencies": {
"@actual-app/api": "*",
@@ -80,7 +79,6 @@
"webpack": "^5.88.2",
"webpack-bundle-analyzer": "^4.9.1",
"webpack-cli": "^5.1.4",
- "ws": "^4.1.0",
"yargs": "^9.0.1"
}
}
diff --git a/packages/loot-core/src/client/actions/app.ts b/packages/loot-core/src/client/actions/app.ts
index 9383b120491..dac6c7bb5ca 100644
--- a/packages/loot-core/src/client/actions/app.ts
+++ b/packages/loot-core/src/client/actions/app.ts
@@ -1,4 +1,4 @@
-import { init as initConnection, send } from '../../platform/client/fetch';
+import { send } from '../../platform/client/fetch';
import * as constants from '../constants';
import type {
AppState,
@@ -15,12 +15,6 @@ export function setAppState(state: Partial): SetAppStateAction {
};
}
-export function reconnect(connectionName: string) {
- return () => {
- initConnection(connectionName);
- };
-}
-
export function updateApp() {
return async (dispatch: Dispatch) => {
global.Actual.applyAppUpdate();
diff --git a/packages/loot-core/src/platform/client/fetch/index.web.ts b/packages/loot-core/src/platform/client/fetch/index.web.ts
index cdf9e2c5fdf..c37e18a26c5 100644
--- a/packages/loot-core/src/platform/client/fetch/index.web.ts
+++ b/packages/loot-core/src/platform/client/fetch/index.web.ts
@@ -8,89 +8,76 @@ const replyHandlers = new Map();
const listeners = new Map();
let messageQueue = [];
let socketClient = null;
-let activePort = null;
-function connectSocket(port, onOpen) {
- // Do nothing if connection to this port is already active
- if (socketClient && port === activePort) {
- return;
- }
+function connectSocket(onOpen) {
+ global.Actual.ipcConnect(function (client) {
+ client.on('message', data => {
+ const msg = data;
+
+ if (msg.type === 'error') {
+ // An error happened while handling a message so cleanup the
+ // current reply handler. We don't care about the actual error -
+ // generic backend errors are handled separately and if you want
+ // more specific handling you should manually forward the error
+ // through a normal reply.
+ const { id } = msg;
+ replyHandlers.delete(id);
+ } else if (msg.type === 'reply') {
+ let { result } = msg;
+ const { id, mutated, undoTag } = msg;
+
+ // Check if the result is a serialized buffer, and if so
+ // convert it to a Uint8Array. This is only needed when working
+ // with node; the web version connection layer automatically
+ // supports buffers
+ if (result && result.type === 'Buffer' && Array.isArray(result.data)) {
+ result = new Uint8Array(result.data);
+ }
- const client = new WebSocket('ws://localhost:' + port);
- socketClient = client;
- activePort = port;
-
- client.onmessage = event => {
- const msg = JSON.parse(event.data);
-
- if (msg.type === 'error') {
- // An error happened while handling a message so cleanup the
- // current reply handler. We don't care about the actual error -
- // generic backend errors are handled separately and if you want
- // more specific handling you should manually forward the error
- // through a normal reply.
- const { id } = msg;
- replyHandlers.delete(id);
- } else if (msg.type === 'reply') {
- let { result } = msg;
- const { id, mutated, undoTag } = msg;
-
- // Check if the result is a serialized buffer, and if so
- // convert it to a Uint8Array. This is only needed when working
- // with node; the web version connection layer automatically
- // supports buffers
- if (result && result.type === 'Buffer' && Array.isArray(result.data)) {
- result = new Uint8Array(result.data);
- }
+ const handler = replyHandlers.get(id);
+ if (handler) {
+ replyHandlers.delete(id);
- const handler = replyHandlers.get(id);
- if (handler) {
- replyHandlers.delete(id);
+ if (!mutated) {
+ undo.gc(undoTag);
+ }
- if (!mutated) {
- undo.gc(undoTag);
+ handler.resolve(result);
}
-
- handler.resolve(result);
- }
- } else if (msg.type === 'push') {
- const { name, args } = msg;
-
- const listens = listeners.get(name);
- if (listens) {
- for (let i = 0; i < listens.length; i++) {
- const stop = listens[i](args);
- if (stop === true) {
- break;
+ } else if (msg.type === 'push') {
+ const { name, args } = msg;
+
+ const listens = listeners.get(name);
+ if (listens) {
+ for (let i = 0; i < listens.length; i++) {
+ const stop = listens[i](args);
+ if (stop === true) {
+ break;
+ }
}
}
+ } else {
+ throw new Error('Unknown message type: ' + JSON.stringify(msg));
}
- } else {
- throw new Error('Unknown message type: ' + JSON.stringify(msg));
- }
- };
+ });
+
+ socketClient = client;
- client.onopen = event => {
// Send any messages that were queued while closed
if (messageQueue.length > 0) {
- messageQueue.forEach(msg => {
- socketClient.send(msg);
- });
+ messageQueue.forEach(msg => client.emit('message', msg));
messageQueue = [];
}
onOpen();
- };
-
- client.onclose = () => {
- socketClient = null;
- };
+ });
}
-export const init: T.Init = async function (socketName) {
- return new Promise(resolve => connectSocket(socketName, resolve));
+export const init: T.Init = async function () {
+ return new Promise(connectSocket);
};
+// @ts-expect-error Figure out why typechecker is suddenly breaking here
export const send: T.Send = function (
name,
args,
@@ -101,28 +88,23 @@ export const send: T.Send = function (
replyHandlers.set(id, { resolve, reject });
if (socketClient) {
- socketClient.send(
- JSON.stringify({
- id,
- name,
- args,
- undoTag: undo.snapshot(),
- catchErrors: !!catchErrors,
- }),
- );
+ socketClient.emit('message', {
+ id,
+ name,
+ args,
+ undoTag: undo.snapshot(),
+ catchErrors: !!catchErrors,
+ });
} else {
- messageQueue.push(
- JSON.stringify({
- id,
- name,
- args,
- undoTag: undo.snapshot(),
- catchErrors,
- }),
- );
+ messageQueue.push({
+ id,
+ name,
+ args,
+ undoTag: undo.snapshot(),
+ catchErrors,
+ });
}
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- }) as any;
+ });
};
export const sendCatch: T.SendCatch = function (name, args) {
diff --git a/packages/loot-core/src/platform/server/connection/index.electron.ts b/packages/loot-core/src/platform/server/connection/index.electron.ts
index 92206ff419c..4f974477fd3 100644
--- a/packages/loot-core/src/platform/server/connection/index.electron.ts
+++ b/packages/loot-core/src/platform/server/connection/index.electron.ts
@@ -3,12 +3,6 @@ import { captureException } from '../../exceptions';
import type * as T from '.';
-// for some reason import doesn't work
-const WebSocketServer = require('ws').Server;
-
-// the websocket server
-let wss = null;
-
function coerceError(error) {
if (error.type && error.type === 'APIError') {
return error;
@@ -17,105 +11,74 @@ function coerceError(error) {
return { type: 'InternalError', message: error.message };
}
-export const init: T.Init = function (socketName, handlers) {
- wss = new WebSocketServer({ port: socketName });
-
- // websockets doesn't support sending objects so parse/stringify needed
- wss.on('connection', function connection(ws) {
- ws.on('error', console.error);
-
- ws.on('message', data => {
- const msg = JSON.parse(data);
-
- if (ws.readyState !== 1) {
- return;
- }
-
- const { id, name, args, undoTag, catchErrors } = msg;
+export const init: T.Init = function (_socketName, handlers) {
+ process.parentPort.on('message', ({ data }) => {
+ const { id, name, args, undoTag, catchErrors } = data;
- if (handlers[name]) {
- runHandler(handlers[name], args, { undoTag, name }).then(
- result => {
- if (ws.readyState !== 1) {
- return;
- }
- if (catchErrors) {
- result = { data: result, error: null };
- }
+ if (handlers[name]) {
+ runHandler(handlers[name], args, { undoTag, name }).then(
+ result => {
+ if (catchErrors) {
+ result = { data: result, error: null };
+ }
- ws.send(
- JSON.stringify({
- type: 'reply',
- id,
- result,
- mutated:
- isMutating(handlers[name]) &&
- name !== 'undo' &&
- name !== 'redo',
- undoTag,
- }),
- );
- },
- nativeError => {
- if (ws.readyState !== 1) {
- return;
- }
- const error = coerceError(nativeError);
-
- if (name.startsWith('api/')) {
- // The API is newer and does automatically forward
- // errors
- ws.send(JSON.stringify({ type: 'reply', id, error }));
- } else if (catchErrors) {
- ws.send(
- JSON.stringify({
- type: 'reply',
- id,
- result: { error, data: null },
- }),
- );
- } else {
- ws.send(JSON.stringify({ type: 'error', id }));
- }
-
- if (error.type === 'InternalError' && name !== 'api/load-budget') {
- captureException(nativeError);
- }
-
- if (!catchErrors) {
- // Notify the frontend that something bad happend
- send('server-error');
- }
- },
- );
- } else {
- console.warn('Unknown method: ' + name);
- captureException(new Error('Unknown server method: ' + name));
- ws.send(
- JSON.stringify({
+ process.parentPort.postMessage({
type: 'reply',
id,
- result: null,
- error: { type: 'APIError', message: 'Unknown method: ' + name },
- }),
- );
- }
- });
+ result,
+ mutated:
+ isMutating(handlers[name]) && name !== 'undo' && name !== 'redo',
+ undoTag,
+ });
+ },
+ nativeError => {
+ const error = coerceError(nativeError);
+
+ if (name.startsWith('api/')) {
+ // The API is newer and does automatically forward
+ // errors
+ process.parentPort.postMessage({
+ type: 'reply',
+ id,
+ error,
+ });
+ } else if (catchErrors) {
+ process.parentPort.postMessage({
+ type: 'reply',
+ id,
+ result: { error, data: null },
+ });
+ } else {
+ process.parentPort.postMessage({ type: 'error', id });
+ }
+
+ if (error.type === 'InternalError' && name !== 'api/load-budget') {
+ captureException(nativeError);
+ }
+
+ if (!catchErrors) {
+ // Notify the frontend that something bad happend
+ send('server-error');
+ }
+ },
+ );
+ } else {
+ console.warn('Unknown method: ' + name);
+ captureException(new Error('Unknown server method: ' + name));
+ process.parentPort.postMessage({
+ type: 'reply',
+ id,
+ result: null,
+ error: { type: 'APIError', message: 'Unknown method: ' + name },
+ });
+ }
});
};
export const getNumClients: T.GetNumClients = function () {
- if (wss) {
- return wss.clients.length;
- }
-
return 0;
};
export const send: T.Send = function (name, args) {
- if (wss) {
- wss.clients.forEach(client =>
- client.send(JSON.stringify({ type: 'push', name, args })),
- );
- }
+ process.parentPort.postMessage({ type: 'push', name, args });
};
diff --git a/packages/loot-core/src/server/budget/goals/goalsSchedule.ts b/packages/loot-core/src/server/budget/goals/goalsSchedule.ts
index b311d5bcf9e..f71a0f7f379 100644
--- a/packages/loot-core/src/server/budget/goals/goalsSchedule.ts
+++ b/packages/loot-core/src/server/budget/goals/goalsSchedule.ts
@@ -8,82 +8,71 @@ import {
} from '../../schedules/app';
import { isReflectBudget } from '../actions';
-export async function goalsSchedule(
- scheduleFlag,
- template_lines,
- current_month,
- balance,
- remainder,
- last_month_balance,
- to_budget,
- errors,
- category,
-) {
- if (!scheduleFlag) {
- scheduleFlag = true;
- const template = template_lines.filter(t => t.type === 'schedule');
- //in the case of multiple templates per category, schedules may have wrong priority level
- let t = [];
- let totalScheduledGoal = 0;
+async function createScheduleList(template, current_month, category) {
+ const t = [];
+ const errors = [];
- for (let ll = 0; ll < template.length; ll++) {
- const { id: sid, completed: complete } = await db.first(
- 'SELECT * FROM schedules WHERE name = ?',
- [template[ll].name],
- );
- const rule = await getRuleForSchedule(sid);
- const conditions = rule.serialize().conditions;
- const { date: dateConditions, amount: amountCondition } =
- extractScheduleConds(conditions);
- const sign = category.is_income ? 1 : -1;
- const target =
- amountCondition.op === 'isbetween'
- ? (sign *
- Math.round(
- amountCondition.value.num1 + amountCondition.value.num2,
- )) /
- 2
- : sign * amountCondition.value;
- const next_date_string = getNextDate(
- dateConditions,
- monthUtils._parse(current_month),
- );
- const target_interval = dateConditions.value.interval
- ? dateConditions.value.interval
- : 1;
- const target_frequency = dateConditions.value.frequency;
- const isRepeating =
- Object(dateConditions.value) === dateConditions.value &&
- 'frequency' in dateConditions.value;
- const num_months = monthUtils.differenceInCalendarMonths(
- next_date_string,
- current_month,
- );
- const startDate = dateConditions.value.start ?? dateConditions.value;
- const started = startDate <= monthUtils.addMonths(current_month, 1);
+ for (let ll = 0; ll < template.length; ll++) {
+ const { id: sid, completed: complete } = await db.first(
+ 'SELECT * FROM schedules WHERE name = ? AND tombstone = 0',
+ [template[ll].name],
+ );
+ const rule = await getRuleForSchedule(sid);
+ const conditions = rule.serialize().conditions;
+ const { date: dateConditions, amount: amountCondition } =
+ extractScheduleConds(conditions);
+ const sign = category.is_income ? 1 : -1;
+ const target =
+ amountCondition.op === 'isbetween'
+ ? (sign *
+ Math.round(
+ amountCondition.value.num1 + amountCondition.value.num2,
+ )) /
+ 2
+ : sign * amountCondition.value;
+ const next_date_string = getNextDate(
+ dateConditions,
+ monthUtils._parse(current_month),
+ );
+ const target_interval = dateConditions.value.interval
+ ? dateConditions.value.interval
+ : 1;
+ const target_frequency = dateConditions.value.frequency;
+ const isRepeating =
+ Object(dateConditions.value) === dateConditions.value &&
+ 'frequency' in dateConditions.value;
+ const num_months = monthUtils.differenceInCalendarMonths(
+ next_date_string,
+ current_month,
+ );
+ if (num_months < 0) {
+ //non-repeating schedules could be negative
+ errors.push(`Schedule ${template[ll].name} is in the Past.`);
+ } else {
t.push({
- template: template[ll],
target,
next_date_string,
target_interval,
target_frequency,
num_months,
completed: complete,
- started,
+ //started,
+ full: template[ll].full === null ? false : template[ll].full,
+ repeat: isRepeating,
+ name: template[ll].name,
});
- if (!complete && started) {
+ if (!complete) {
if (isRepeating) {
let monthlyTarget = 0;
const nextMonth = monthUtils.addMonths(
current_month,
- t[ll].num_months + 1,
+ t[t.length - 1].num_months + 1,
);
let nextBaseDate = getNextDate(
dateConditions,
monthUtils._parse(current_month),
true,
);
-
let nextDate = dateConditions.value.skipWeekend
? monthUtils.dayFromDate(
getDateWithSkippedWeekend(
@@ -92,7 +81,6 @@ export async function goalsSchedule(
),
)
: nextBaseDate;
-
while (nextDate < nextMonth) {
monthlyTarget += -target;
const currentDate = nextBaseDate;
@@ -119,92 +107,129 @@ export async function goalsSchedule(
break;
}
}
- t[ll].target = -monthlyTarget;
- totalScheduledGoal += target;
+ t[t.length - 1].target = -monthlyTarget;
}
} else {
errors.push(
- `Schedule ${t[ll].template.name} is not active during the month in question.`,
+ `Schedule ${t[ll].name} is not active during the month in question.`,
);
}
}
+ }
+ return { t: t.filter(c => c.completed === 0), errors };
+}
- t = t.filter(t => t.completed === 0 && t.started);
- t = t.sort((a, b) => b.target - a.target);
+async function getPayMonthOfTotal(t) {
+ //return the contribution amounts of full or every month type schedules
+ let total = 0;
+ const schedules = t.filter(c => c.num_months === 0);
+ for (let ll = 0; ll < schedules.length; ll++) {
+ total += schedules[ll].target;
+ }
+ return total;
+}
- let increment = 0;
- if (balance >= totalScheduledGoal) {
- for (let ll = 0; ll < t.length; ll++) {
- if (t[ll].num_months < 0) {
- errors.push(
- `Non-repeating schedule ${t[ll].template.name} was due on ${t[ll].next_date_string}, which is in the past.`,
- );
- break;
- }
- if (
- (t[ll].template.full && t[ll].num_months === 0) ||
- t[ll].target_frequency === 'weekly' ||
- t[ll].target_frequency === 'daily'
- ) {
- increment += t[ll].target;
- } else if (t[ll].template.full && t[ll].num_months > 0) {
- increment += 0;
- } else {
- increment += t[ll].target / t[ll].target_interval;
- }
- }
- } else if (balance < totalScheduledGoal) {
- for (let ll = 0; ll < t.length; ll++) {
- if (isReflectBudget()) {
- if (!t[ll].template.full) {
- errors.push(
- `Report budgets require the full option for Schedules.`,
- );
- break;
- }
- if (t[ll].template.full && t[ll].num_months === 0) {
- to_budget += t[ll].target;
- }
- }
- if (!isReflectBudget()) {
- if (t[ll].num_months < 0) {
- errors.push(
- `Non-repeating schedule ${t[ll].template.name} was due on ${t[ll].next_date_string}, which is in the past.`,
- );
- break;
- }
- if (t[ll].template.full && t[ll].num_months > 0) {
- remainder = 0;
- } else if (ll === 0 && !t[ll].template.full) {
- remainder = t[ll].target - last_month_balance;
- } else {
- remainder = t[ll].target - remainder;
- }
- let tg = 0;
- if (remainder >= 0) {
- tg = remainder;
- remainder = 0;
- } else {
- tg = 0;
- remainder = Math.abs(remainder);
- }
- if (
- t[ll].template.full ||
- t[ll].num_months === 0 ||
- t[ll].target_frequency === 'weekly' ||
- t[ll].target_frequency === 'daily'
- ) {
- increment += tg;
- } else if (t[ll].template.full && t[ll].num_months > 0) {
- increment += 0;
- } else {
- increment += tg / (t[ll].num_months + 1);
- }
- }
- }
+async function getSinkingContributionTotal(t, remainder, last_month_balance) {
+ //return the contribution amount if there is a balance carried in the category
+ let total = 0;
+ for (let ll = 0; ll < t.length; ll++) {
+ remainder =
+ ll === 0 ? t[ll].target - last_month_balance : t[ll].target - remainder;
+ let tg = 0;
+ if (remainder >= 0) {
+ tg = remainder;
+ remainder = 0;
+ } else {
+ tg = 0;
+ remainder = Math.abs(remainder);
+ }
+ total += tg / (t[ll].num_months + 1);
+ }
+ return total;
+}
+
+async function getSinkingBaseContributionTotal(t) {
+ //return only the base contribution of each schedule
+ let total = 0;
+ for (let ll = 0; ll < t.length; ll++) {
+ total += t[ll].target / t[ll].target_interval;
+ }
+ return total;
+}
+
+async function getSinkingTotal(t) {
+ //sum the total of all upcoming schedules
+ let total = 0;
+ for (let ll = 0; ll < t.length; ll++) {
+ total += t[ll].target;
+ }
+ return total;
+}
+
+export async function goalsSchedule(
+ scheduleFlag,
+ template_lines,
+ current_month,
+ balance,
+ remainder,
+ last_month_balance,
+ to_budget,
+ errors,
+ category,
+) {
+ if (!scheduleFlag) {
+ scheduleFlag = true;
+ const template = template_lines.filter(t => t.type === 'schedule');
+ //in the case of multiple templates per category, schedules may have wrong priority level
+
+ const t = await createScheduleList(template, current_month, category);
+ errors = errors.concat(t.errors);
+
+ const t_payMonthOf = t.t.filter(
+ c =>
+ c.full ||
+ (c.target_frequency === 'monthly' &&
+ c.target_interval === 1 &&
+ c.num_months === 0) ||
+ (c.target_frequency === 'weekly' &&
+ c.target_interval >= 0 &&
+ c.num_months === 0) ||
+ c.target_frequency === 'daily' ||
+ isReflectBudget(),
+ );
+
+ const t_sinking = t.t
+ .filter(
+ c =>
+ (!c.full &&
+ c.target_frequency === 'monthly' &&
+ c.target_interval > 1) ||
+ (!c.full &&
+ c.target_frequency === 'monthly' &&
+ c.num_months > 0 &&
+ c.target_interval === 1) ||
+ (!c.full && c.target_frequency === 'yearly') ||
+ (!c.full && c.target_frequency === undefined),
+ )
+ .sort((a, b) => a.next_date_string.localeCompare(b.next_date_string));
+
+ const totalPayMonthOf = await getPayMonthOfTotal(t_payMonthOf);
+
+ const totalSinking = await getSinkingTotal(t_sinking);
+ const totalSinkingBaseContribution = await getSinkingBaseContributionTotal(
+ t_sinking,
+ );
+
+ if (balance >= totalSinking + totalPayMonthOf) {
+ to_budget += Math.round(totalPayMonthOf + totalSinkingBaseContribution);
+ } else {
+ const totalSinkingContribution = await getSinkingContributionTotal(
+ t_sinking,
+ remainder,
+ last_month_balance,
+ );
+ to_budget += Math.round(totalPayMonthOf + totalSinkingContribution);
}
- increment = Math.round(increment);
- to_budget += increment;
}
return { to_budget, errors, remainder, scheduleFlag };
}
diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts
index d16ec1093da..8d8da4116ec 100644
--- a/packages/loot-core/src/server/main.ts
+++ b/packages/loot-core/src/server/main.ts
@@ -1348,7 +1348,10 @@ handlers['save-global-prefs'] = async function (prefs) {
}
if ('autoUpdate' in prefs) {
await asyncStorage.setItem('auto-update', '' + prefs.autoUpdate);
- process.send({ type: 'shouldAutoUpdate', flag: prefs.autoUpdate });
+ process.parentPort.postMessage({
+ type: 'shouldAutoUpdate',
+ flag: prefs.autoUpdate,
+ });
}
if ('documentDir' in prefs) {
if (await fs.exists(prefs.documentDir)) {
@@ -2235,7 +2238,7 @@ export async function initApp(isDev, socketName) {
if (!isDev && !Platform.isMobile && !Platform.isWeb) {
const autoUpdate = await asyncStorage.getItem('auto-update');
- process.send({
+ process.parentPort.postMessage({
type: 'shouldAutoUpdate',
flag: autoUpdate == null || autoUpdate === 'true',
});
diff --git a/packages/loot-core/webpack/webpack.desktop.config.js b/packages/loot-core/webpack/webpack.desktop.config.js
index a0f9b2c6dc2..b1df8510040 100644
--- a/packages/loot-core/webpack/webpack.desktop.config.js
+++ b/packages/loot-core/webpack/webpack.desktop.config.js
@@ -28,14 +28,7 @@ module.exports = {
'pegjs',
],
},
- externals: [
- 'better-sqlite3',
- 'electron-log',
- 'node-fetch',
- 'node-libofx',
- 'ws',
- 'fs',
- ],
+ externals: ['better-sqlite3', 'electron-log', 'node-fetch', 'node-libofx'],
plugins: [
new webpack.IgnorePlugin({
resourceRegExp: /original-fs/,
diff --git a/upcoming-release-notes/2102.md b/upcoming-release-notes/2102.md
new file mode 100644
index 00000000000..a632e1dd315
--- /dev/null
+++ b/upcoming-release-notes/2102.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [shall0pass]
+---
+
+Goals: Refactor schedules file into functions and improve the readability of the code.
\ No newline at end of file
diff --git a/upcoming-release-notes/2153.md b/upcoming-release-notes/2153.md
new file mode 100644
index 00000000000..8fab8116d45
--- /dev/null
+++ b/upcoming-release-notes/2153.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [carkom]
+---
+
+Reorganize tableGraph files for custom reports.
diff --git a/upcoming-release-notes/2163.md b/upcoming-release-notes/2163.md
new file mode 100644
index 00000000000..2d7313306ba
--- /dev/null
+++ b/upcoming-release-notes/2163.md
@@ -0,0 +1,6 @@
+---
+category: Enhancements
+authors: [ScottFries, blakegearin, carkom]
+---
+
+Add ability to import categories from CSV
\ No newline at end of file
diff --git a/upcoming-release-notes/2190.md b/upcoming-release-notes/2190.md
new file mode 100644
index 00000000000..0e78c730bdf
--- /dev/null
+++ b/upcoming-release-notes/2190.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [MatissJanis]
+---
+
+electron: move back from WebSockets to IPC for internal communications. This should improve the stability of the desktop app.
diff --git a/upcoming-release-notes/2195.md b/upcoming-release-notes/2195.md
new file mode 100644
index 00000000000..597a969a7a5
--- /dev/null
+++ b/upcoming-release-notes/2195.md
@@ -0,0 +1,6 @@
+---
+category: Bugfix
+authors: [youngcw]
+---
+
+Add missing borders in report budget table
diff --git a/upcoming-release-notes/2196.md b/upcoming-release-notes/2196.md
new file mode 100644
index 00000000000..8ef2f5e0a80
--- /dev/null
+++ b/upcoming-release-notes/2196.md
@@ -0,0 +1,6 @@
+---
+category: Bugfix
+authors: [youngcw]
+---
+
+Improve report budget pie chart colors
diff --git a/yarn.lock b/yarn.lock
index de0c15aebb4..5eadc1b0dd3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5924,13 +5924,6 @@ __metadata:
languageName: node
linkType: hard
-"async-limiter@npm:~1.0.0":
- version: 1.0.1
- resolution: "async-limiter@npm:1.0.1"
- checksum: 2b849695b465d93ad44c116220dee29a5aeb63adac16c1088983c339b0de57d76e82533e8e364a93a9f997f28bbfc6a92948cefc120652bd07f3b59f8d75cf2b
- languageName: node
- linkType: hard
-
"async@npm:^3.2.3":
version: 3.2.4
resolution: "async@npm:3.2.4"
@@ -8280,11 +8273,9 @@ __metadata:
electron-is-dev: "npm:2.0.0"
electron-log: "npm:4.4.8"
electron-updater: "npm:6.1.7"
- get-port-please: "npm:3.0.1"
loot-core: "npm:*"
node-fetch: "npm:^2.6.9"
promise-retry: "npm:^2.0.1"
- ws: "npm:8.13.0"
languageName: unknown
linkType: soft
@@ -10410,13 +10401,6 @@ __metadata:
languageName: node
linkType: hard
-"get-port-please@npm:3.0.1":
- version: 3.0.1
- resolution: "get-port-please@npm:3.0.1"
- checksum: a5de771314986e45872354a72e24f27c13884c14be9e00b7332bc53de971c50787d2ae61dd81ef6d1eb2df5c35b1e3528906de29ac4a5127769f7ebee6e8f100
- languageName: node
- linkType: hard
-
"get-proxy@npm:^2.0.0":
version: 2.1.0
resolution: "get-proxy@npm:2.1.0"
@@ -13417,7 +13401,6 @@ __metadata:
webpack: "npm:^5.88.2"
webpack-bundle-analyzer: "npm:^4.9.1"
webpack-cli: "npm:^5.1.4"
- ws: "npm:^4.1.0"
yargs: "npm:^9.0.1"
languageName: unknown
linkType: soft
@@ -21563,43 +21546,33 @@ __metadata:
languageName: node
linkType: hard
-"ws@npm:8.13.0, ws@npm:^8.13.0":
- version: 8.13.0
- resolution: "ws@npm:8.13.0"
+"ws@npm:^7.3.1, ws@npm:^7.4.6":
+ version: 7.5.9
+ resolution: "ws@npm:7.5.9"
peerDependencies:
bufferutil: ^4.0.1
- utf-8-validate: ">=5.0.2"
+ utf-8-validate: ^5.0.2
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
- checksum: 1769532b6fdab9ff659f0b17810e7501831d34ecca23fd179ee64091dd93a51f42c59f6c7bb4c7a384b6c229aca8076fb312aa35626257c18081511ef62a161d
- languageName: node
- linkType: hard
-
-"ws@npm:^4.1.0":
- version: 4.1.0
- resolution: "ws@npm:4.1.0"
- dependencies:
- async-limiter: "npm:~1.0.0"
- safe-buffer: "npm:~5.1.0"
- checksum: afdb3c55defe9ff39474fd769f7a7c718e3fa868b8bc945546bdcb1277ee0bc086c19ff688e551fdcd2a2359218724f9088acfad4bb05a6c7afc2068ea9ec144
+ checksum: 171e35012934bd8788150a7f46f963e50bac43a4dc524ee714c20f258693ac4d3ba2abadb00838fdac42a47af9e958c7ae7e6f4bc56db047ba897b8a2268cf7c
languageName: node
linkType: hard
-"ws@npm:^7.3.1, ws@npm:^7.4.6":
- version: 7.5.9
- resolution: "ws@npm:7.5.9"
+"ws@npm:^8.13.0":
+ version: 8.13.0
+ resolution: "ws@npm:8.13.0"
peerDependencies:
bufferutil: ^4.0.1
- utf-8-validate: ^5.0.2
+ utf-8-validate: ">=5.0.2"
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
- checksum: 171e35012934bd8788150a7f46f963e50bac43a4dc524ee714c20f258693ac4d3ba2abadb00838fdac42a47af9e958c7ae7e6f4bc56db047ba897b8a2268cf7c
+ checksum: 1769532b6fdab9ff659f0b17810e7501831d34ecca23fd179ee64091dd93a51f42c59f6c7bb4c7a384b6c229aca8076fb312aa35626257c18081511ef62a161d
languageName: node
linkType: hard