Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TypeScript] Add types to SpreadsheetProvider #4409

Merged
merged 4 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,38 +1,26 @@
import React, {
useState,
useContext,
useEffect,
type ChangeEvent,
} from 'react';
import React, { useState, type ChangeEvent } from 'react';
import { Trans } from 'react-i18next';

import { Button } from '@actual-app/components/button';
import { InitialFocus } from '@actual-app/components/initial-focus';
import { View } from '@actual-app/components/view';

import { useSpreadsheet } from 'loot-core/client/SpreadsheetProvider';
import { evalArithmetic } from 'loot-core/shared/arithmetic';
import { integerToCurrency, amountToInteger } from 'loot-core/shared/util';

import { Input } from '../../common/Input';
import { NamespaceContext } from '../../spreadsheet/NamespaceContext';
import { useSheetValue } from '../../spreadsheet/useSheetValue';

type HoldMenuProps = {
onSubmit: (amount: number) => void;
onClose: () => void;
};
export function HoldMenu({ onSubmit, onClose }: HoldMenuProps) {
const spreadsheet = useSpreadsheet();
const sheetName = useContext(NamespaceContext);

const [amount, setAmount] = useState<string | null>(null);

useEffect(() => {
(async () => {
const node = await spreadsheet.get(sheetName, 'to-budget');
setAmount(integerToCurrency(Math.max(node.value as number, 0)));
})();
}, []);
useSheetValue<'envelope-budget', 'to-budget'>('to-budget', ({ value }) => {
setAmount(integerToCurrency(Math.max(value || 0, 0)));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Replaced with useSheetValue which does the same thing

});

function submit(newAmount: string) {
const parsedAmount = evalArithmetic(newAmount);
Expand Down
35 changes: 17 additions & 18 deletions packages/desktop-client/src/components/spreadsheet/useSheetValue.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useState, useRef, useLayoutEffect, useMemo } from 'react';

import { useSpreadsheet } from 'loot-core/client/SpreadsheetProvider';
import { type Query } from 'loot-core/shared/query';

import { useSheetName } from './useSheetName';

Expand All @@ -18,7 +17,6 @@ type SheetValueResult<
> = {
name: string;
value: Spreadsheets[SheetName][FieldName] | null;
query?: Query;
};

export function useSheetValue<
Expand All @@ -42,7 +40,6 @@ export function useSheetValue<
const [result, setResult] = useState<SheetValueResult<SheetName, FieldName>>({
name: fullSheetName,
value: bindingObj.value ? bindingObj.value : null,
query: bindingObj.query,
});
const latestOnChange = useRef(onChange);
latestOnChange.current = onChange;
Expand All @@ -53,23 +50,25 @@ export function useSheetValue<
useLayoutEffect(() => {
let isMounted = true;

const unbind = spreadsheet.bind(
sheetName,
bindingObj,
(newResult: SheetValueResult<SheetName, FieldName>) => {
if (!isMounted) {
return;
}
const unbind = spreadsheet.bind(sheetName, bindingObj, newResult => {
if (!isMounted) {
return;
}

if (latestOnChange.current) {
latestOnChange.current(newResult);
}
const newCastedResult = {
name: newResult.name,
// TODO: Spreadsheets, SheetNames, SheetFields, etc must be moved to the loot-core package
value: newResult.value as Spreadsheets[SheetName][FieldName],
};
Comment on lines +58 to +62
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Consider moving type definitions to improve code organization.

The TODO comment indicates that type definitions should be moved to the loot-core package. This would improve code organization and make types more accessible across the project.

Let's verify the current location of type definitions:


🏁 Script executed:

#!/bin/bash
# Search for type definitions across the codebase
rg -t typescript "type (Spreadsheets|SheetNames|SheetFields)" -g '!dist' -g '!node_modules'

Length of output: 128


I've noticed that the initial search command failed because of an unrecognized file type. To gather more reliable information we should rerun the search using a glob that targets TypeScript files. For example:

Once you verify where these type definitions are currently declared (or if they're even declared here), please confirm whether they might benefit from being centralized in the loot-core package as suggested by the TODO comment.


Action: Verify the location of the type definitions for Spreadsheets, SheetNames, and SheetFields, and consider migrating them into the loot-core package for improved organization and reusability.

  • File in question: packages/desktop-client/src/components/spreadsheet/useSheetValue.ts (Lines 58-62)
  • Suggestion: Centralize these type definitions (currently defined locally or imported inconsistently) in the loot-core package to avoid duplication and improve maintainability.

🏁 Script executed:

[run_scripts]
#!/bin/bash
# Corrected search for type definitions across TypeScript files in the codebase
rg "type\s+(Spreadsheets|SheetNames|SheetFields)" -g "*.{ts,tsx}" -g '!dist' -g '!node_modules'

Length of output: 2241


Consolidate Spreadsheet Type Definitions Across Packages

  • The TODO in packages/desktop-client/src/components/spreadsheet/useSheetValue.ts advises moving spreadsheet-related types to the loot-core package.
  • Verification shows that SheetNames and SheetFields are already defined in packages/loot-core/src/client/queries.ts, while Spreadsheets is still defined under the desktop-client (see packages/desktop-client/src/components/spreadsheet/index.ts).
  • To improve maintainability and ensure all related types are centralized, consider moving the Spreadsheets type—and possibly consolidating all spreadsheet types—into the loot-core package and updating the imports accordingly.


if (newResult.value !== latestValue.current) {
setResult(newResult);
}
},
);
if (latestOnChange.current) {
latestOnChange.current(newCastedResult);
}

if (newResult.value !== latestValue.current) {
setResult(newCastedResult);
}
});

return () => {
isMounted = false;
Expand Down
2 changes: 1 addition & 1 deletion packages/loot-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"date-fns": "^2.30.0",
"deep-equal": "^2.2.3",
"handlebars": "^4.7.8",
"lru-cache": "^5.1.1",
"lru-cache": "^11.0.2",
"md5": "^2.3.0",
"memoize-one": "^6.0.0",
"mitt": "^3.0.1",
Expand Down
82 changes: 59 additions & 23 deletions packages/loot-core/src/client/SpreadsheetProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,81 @@
// @ts-strict-ignore
import React, { createContext, useEffect, useMemo, useContext } from 'react';
import {
createContext,
useEffect,
useMemo,
useContext,
type ReactNode,
} from 'react';

import LRU from 'lru-cache';
import { LRUCache } from 'lru-cache';

import { listen, send } from '../platform/client/fetch';
import { type Node } from '../server/spreadsheet/spreadsheet';
import { type Query } from '../shared/query';

type SpreadsheetContextValue = ReturnType<typeof makeSpreadsheet>;
const SpreadsheetContext = createContext<SpreadsheetContextValue>(undefined);
const SpreadsheetContext = createContext<SpreadsheetContextValue | undefined>(
undefined,
);

export function useSpreadsheet() {
return useContext(SpreadsheetContext);
const context = useContext(SpreadsheetContext);
if (!context) {
throw new Error('useSpreadsheet must be used within a SpreadsheetProvider');
}
return context;
}

// TODO: Make this generic and replace the Binding type in the desktop-client package.
type Binding =
| string
| { name: string; value?: unknown | null; query?: Query | undefined };

type CellCacheValue = { name: string; value: Node['value'] | null };
type CellCache = { [name: string]: Promise<CellCacheValue> | null };
type CellObserverCallback = (node: CellCacheValue) => void;
type CellObservers = { [name: string]: CellObserverCallback[] };

const GLOBAL_SHEET_NAME = '__global';

function makeSpreadsheet() {
const cellObservers = {};
const LRUValueCache = new LRU({ max: 1200 });
const cellCache = {};
const cellObservers: CellObservers = {};
const LRUValueCache = new LRUCache<string, CellCacheValue>({ max: 1200 });
const cellCache: CellCache = {};
let observersDisabled = false;

class Spreadsheet {
observeCell(name, cb) {
observeCell(name: string, callback: CellObserverCallback): () => void {
if (!cellObservers[name]) {
cellObservers[name] = [];
}
cellObservers[name].push(cb);
cellObservers[name].push(callback);

return () => {
cellObservers[name] = cellObservers[name].filter(x => x !== cb);
cellObservers[name] = cellObservers[name].filter(cb => cb !== callback);

if (cellObservers[name].length === 0) {
cellCache[name] = null;
}
};
}

disableObservers() {
disableObservers(): void {
observersDisabled = true;
}

enableObservers() {
enableObservers(): void {
observersDisabled = false;
}

prewarmCache(name, value) {
prewarmCache(name: string, value: CellCacheValue): void {
LRUValueCache.set(name, value);
}

listen() {
return listen('cells-changed', function (nodes) {
listen(): () => void {
return listen('cells-changed', event => {
if (!observersDisabled) {
// TODO: batch react so only renders once
nodes.forEach(node => {
event.forEach(node => {
const observers = cellObservers[node.name];
if (observers) {
observers.forEach(func => func(node));
Expand All @@ -62,7 +87,11 @@ function makeSpreadsheet() {
});
}

bind(sheetName = '__global', binding, callback) {
bind(
sheetName: string = GLOBAL_SHEET_NAME,
binding: Binding,
callback: CellObserverCallback,
): () => void {
binding =
typeof binding === 'string' ? { name: binding, value: null } : binding;

Expand All @@ -77,7 +106,10 @@ function makeSpreadsheet() {
// This is a display optimization to avoid flicker. The LRU cache
// will keep a number of recent nodes in memory.
if (LRUValueCache.has(resolvedName)) {
callback(LRUValueCache.get(resolvedName));
const node = LRUValueCache.get(resolvedName);
if (node) {
callback(node);
}
}

if (cellCache[resolvedName] != null) {
Expand All @@ -102,15 +134,15 @@ function makeSpreadsheet() {
return cleanup;
}

get(sheetName, name) {
get(sheetName: string, name: string) {
return send('getCell', { sheetName, name });
}

getCellNames(sheetName) {
getCellNames(sheetName: string) {
return send('getCellNamesInSheet', { sheetName });
}

createQuery(sheetName, name, query) {
createQuery(sheetName: string, name: string, query: Query) {
return send('create-query', {
sheetName,
name,
Expand All @@ -122,7 +154,11 @@ function makeSpreadsheet() {
return new Spreadsheet();
}

export function SpreadsheetProvider({ children }) {
type SpreadsheetProviderProps = {
children: ReactNode;
};

export function SpreadsheetProvider({ children }: SpreadsheetProviderProps) {
const spreadsheet = useMemo(() => makeSpreadsheet(), []);

useEffect(() => {
Expand Down
5 changes: 2 additions & 3 deletions packages/loot-core/src/platform/server/sqlite/unicodeLike.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// @ts-strict-ignore
import LRU from 'lru-cache';
import { LRUCache } from 'lru-cache';

const likePatternCache = new LRU({ max: 500 });
const likePatternCache = new LRUCache<string, RegExp>({ max: 500 });

export function unicodeLike(
pattern: string | null,
Expand Down
6 changes: 3 additions & 3 deletions packages/loot-core/src/server/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
Timestamp,
} from '@actual-app/crdt';
import { Database } from '@jlongster/sql.js';
import LRU from 'lru-cache';
import { LRUCache } from 'lru-cache';
import { v4 as uuidv4 } from 'uuid';

import * as fs from '../../platform/server/fs';
Expand Down Expand Up @@ -132,7 +132,7 @@ export function execQuery(sql: string) {

// This manages an LRU cache of prepared query statements. This is
// only needed in hot spots when you are running lots of queries.
let _queryCache = new LRU({ max: 100 });
let _queryCache = new LRUCache<string, string>({ max: 100 });
export function cache(sql: string) {
const cached = _queryCache.get(sql);
if (cached) {
Expand All @@ -145,7 +145,7 @@ export function cache(sql: string) {
}

function resetQueryCache() {
_queryCache = new LRU({ max: 100 });
_queryCache = new LRUCache<string, string>({ max: 100 });
}

export function transaction(fn: () => void) {
Expand Down
5 changes: 4 additions & 1 deletion packages/loot-core/src/server/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,10 @@ handlers['getCell'] = async function ({ sheetName, name }) {
};

handlers['getCells'] = async function ({ names }) {
return names.map(name => ({ value: sheet.get()._getNode(name).value }));
return names.map(name => {
const node = sheet.get()._getNode(name);
return { name: node.name, value: node.value };
});
};

handlers['getCellNamesInSheet'] = async function ({ sheetName }) {
Expand Down
20 changes: 13 additions & 7 deletions packages/loot-core/src/types/server-handlers.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,18 +130,24 @@ export interface ServerHandlers {
applySpecialCases?: boolean;
}) => Promise<{ filters: unknown[] }>;

getCell: (arg: {
sheetName;
name;
}) => Promise<SpreadsheetNode | { value?: SpreadsheetNode['value'] }>;
getCell: (arg: { sheetName; name }) => Promise<{
name: SpreadsheetNode['name'];
value: SpreadsheetNode['value'];
}>;

getCells: (arg: { names }) => Promise<unknown>;
getCells: (arg: {
names;
}) => Promise<
Array<{ name: SpreadsheetNode['name']; value?: SpreadsheetNode['value'] }>
>;

getCellNamesInSheet: (arg: { sheetName }) => Promise<unknown>;
getCellNamesInSheet: (arg: {
sheetName;
}) => Promise<Array<SpreadsheetNode['name']>>;

debugCell: (arg: { sheetName; name }) => Promise<unknown>;

'create-query': (arg: { sheetName; name; query }) => Promise<unknown>;
'create-query': (arg: { sheetName; name; query }) => Promise<'ok'>;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
query: (query: Query) => Promise<{ data: any; dependencies: string[] }>;
Expand Down
6 changes: 6 additions & 0 deletions packages/loot-core/webpack/webpack.browser.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ module.exports = {
test: /\.pegjs$/,
use: { loader: path.resolve(__dirname, '../peg-loader.js') },
},
{
test: /\.m?js/,
resolve: {
fullySpecified: false,
},
},
],
},
optimization: {
Expand Down
6 changes: 6 additions & 0 deletions upcoming-release-notes/4409.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MikesGlitch]
---

[TypeScript] Add types for SpreadsheetProvider
9 changes: 8 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -16086,7 +16086,7 @@ __metadata:
i18next: "npm:^23.11.5"
jest: "npm:^27.5.1"
jsverify: "npm:^0.8.4"
lru-cache: "npm:^5.1.1"
lru-cache: "npm:^11.0.2"
md5: "npm:^2.3.0"
memfs: "npm:3.5.3"
memoize-one: "npm:^6.0.0"
Expand Down Expand Up @@ -16145,6 +16145,13 @@ __metadata:
languageName: node
linkType: hard

"lru-cache@npm:^11.0.2":
version: 11.0.2
resolution: "lru-cache@npm:11.0.2"
checksum: 10/25fcb66e9d91eaf17227c6abfe526a7bed5903de74f93bfde380eb8a13410c5e8d3f14fe447293f3f322a7493adf6f9f015c6f1df7a235ff24ec30f366e1c058
languageName: node
linkType: hard

"lru-cache@npm:^4.0.1":
version: 4.1.5
resolution: "lru-cache@npm:4.1.5"
Expand Down
Loading