mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-28 10:33:02 -05:00
[Typescript] Server event types (#4110)
* [Typescript] Server event types * Release notes
This commit is contained in:
committed by
GitHub
parent
34ffc5c4b2
commit
92c93b3f6e
@@ -117,8 +117,8 @@ function SyncButton({ style, isMobile = false }: SyncButtonProps) {
|
||||
>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = listen('sync-event', ({ type, subtype, syncDisabled }) => {
|
||||
if (type === 'start') {
|
||||
const unlisten = listen('sync-event', event => {
|
||||
if (event.type === 'start') {
|
||||
setSyncing(true);
|
||||
setSyncState(null);
|
||||
} else {
|
||||
@@ -130,19 +130,19 @@ function SyncButton({ style, isMobile = false }: SyncButtonProps) {
|
||||
}, 200);
|
||||
}
|
||||
|
||||
if (type === 'error') {
|
||||
if (event.type === 'error') {
|
||||
// Use the offline state if either there is a network error or
|
||||
// if this file isn't a "cloud file". You can't sync a local
|
||||
// file.
|
||||
if (subtype === 'network') {
|
||||
if (event.subtype === 'network') {
|
||||
setSyncState('offline');
|
||||
} else if (!cloudFileId) {
|
||||
setSyncState('local');
|
||||
} else {
|
||||
setSyncState('error');
|
||||
}
|
||||
} else if (type === 'success') {
|
||||
setSyncState(syncDisabled ? 'disabled' : null);
|
||||
} else if (event.type === 'success') {
|
||||
setSyncState(event.syncDisabled ? 'disabled' : null);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -108,14 +108,16 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
run();
|
||||
|
||||
const unlistens = [
|
||||
listen('sync-event', ({ type, tables }) => {
|
||||
if (
|
||||
type === 'success' &&
|
||||
(tables.includes('categories') ||
|
||||
listen('sync-event', event => {
|
||||
if (event.type === 'success') {
|
||||
const tables = event.tables;
|
||||
if (
|
||||
tables.includes('categories') ||
|
||||
tables.includes('category_mapping') ||
|
||||
tables.includes('category_groups'))
|
||||
) {
|
||||
loadCategories();
|
||||
tables.includes('category_groups')
|
||||
) {
|
||||
loadCategories();
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
|
||||
@@ -267,8 +267,9 @@ function TransactionListWithPreviews({
|
||||
}, [accountId, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
return listen('sync-event', ({ type, tables }) => {
|
||||
if (type === 'applied') {
|
||||
return listen('sync-event', event => {
|
||||
if (event.type === 'applied') {
|
||||
const tables = event.tables;
|
||||
if (
|
||||
tables.includes('transactions') ||
|
||||
tables.includes('category_mapping') ||
|
||||
|
||||
@@ -62,8 +62,9 @@ export function CategoryTransactions({
|
||||
const dateFormat = useDateFormat() || 'MM/dd/yyyy';
|
||||
|
||||
useEffect(() => {
|
||||
return listen('sync-event', ({ type, tables }) => {
|
||||
if (type === 'applied') {
|
||||
return listen('sync-event', event => {
|
||||
if (event.type === 'applied') {
|
||||
const tables = event.tables;
|
||||
if (
|
||||
tables.includes('transactions') ||
|
||||
tables.includes('category_mapping') ||
|
||||
|
||||
@@ -69,15 +69,17 @@ export function Budget() {
|
||||
|
||||
init();
|
||||
|
||||
const unlisten = listen('sync-event', ({ type, tables }) => {
|
||||
if (
|
||||
type === 'success' &&
|
||||
(tables.includes('categories') ||
|
||||
const unlisten = listen('sync-event', event => {
|
||||
if (event.type === 'success') {
|
||||
const tables = event.tables;
|
||||
if (
|
||||
tables.includes('categories') ||
|
||||
tables.includes('category_mapping') ||
|
||||
tables.includes('category_groups'))
|
||||
) {
|
||||
// TODO: is this loading every time?
|
||||
dispatch(getCategories());
|
||||
tables.includes('category_groups')
|
||||
) {
|
||||
// TODO: is this loading every time?
|
||||
dispatch(getCategories());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -48,9 +48,9 @@ export function ManagePayeesWithData({
|
||||
}
|
||||
loadData();
|
||||
|
||||
const unlisten = listen('sync-event', async ({ type, tables }) => {
|
||||
if (type === 'applied') {
|
||||
if (tables.includes('rules')) {
|
||||
const unlisten = listen('sync-event', async event => {
|
||||
if (event.type === 'applied') {
|
||||
if (event.tables.includes('rules')) {
|
||||
await refetchRuleCounts();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,19 +21,20 @@ export function handleGlobalEvents(actions: BoundActions, store: Store<State>) {
|
||||
});
|
||||
});
|
||||
|
||||
listen('schedules-offline', ({ payees }) => {
|
||||
actions.pushModal('schedule-posts-offline-notification', { payees });
|
||||
listen('schedules-offline', () => {
|
||||
actions.pushModal('schedule-posts-offline-notification');
|
||||
});
|
||||
|
||||
// This is experimental: we sync data locally automatically when
|
||||
// data changes from the backend
|
||||
listen('sync-event', async ({ type, tables }) => {
|
||||
listen('sync-event', async event => {
|
||||
// We don't need to query anything until the file is loaded, and
|
||||
// sync events might come in if the file is being synced before
|
||||
// being loaded (happens when downloading)
|
||||
const prefs = store.getState().prefs.local;
|
||||
if (prefs && prefs.id) {
|
||||
if (type === 'applied') {
|
||||
if (event.type === 'applied') {
|
||||
const tables = event.tables;
|
||||
if (tables.includes('payees') || tables.includes('payee_mapping')) {
|
||||
actions.getPayees();
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export class LiveQuery<TResponse = unknown> {
|
||||
private _data: Data<TResponse>;
|
||||
private _dependencies: Set<string>;
|
||||
private _listeners: Array<Listener<TResponse>>;
|
||||
private _supportedSyncTypes: Set<string>;
|
||||
private _supportedSyncTypes: Set<'applied' | 'success'>;
|
||||
private _query: Query;
|
||||
private _onError: (error: Error) => void;
|
||||
|
||||
@@ -107,8 +107,8 @@ export class LiveQuery<TResponse = unknown> {
|
||||
|
||||
// TODO: error types?
|
||||
this._supportedSyncTypes = options.onlySync
|
||||
? new Set<string>(['success'])
|
||||
: new Set<string>(['applied', 'success']);
|
||||
? new Set(['success'])
|
||||
: new Set(['applied', 'success']);
|
||||
|
||||
if (onData) {
|
||||
this.addListener(onData);
|
||||
@@ -162,15 +162,18 @@ export class LiveQuery<TResponse = unknown> {
|
||||
|
||||
protected subscribe = () => {
|
||||
if (this._unsubscribeSyncEvent == null) {
|
||||
this._unsubscribeSyncEvent = listen('sync-event', ({ type, tables }) => {
|
||||
this._unsubscribeSyncEvent = listen('sync-event', event => {
|
||||
// If the user is doing optimistic updates, they don't want to
|
||||
// always refetch whenever something changes because it would
|
||||
// refetch all data after they've already updated the UI. This
|
||||
// voids the perf benefits of optimistic updates. Allow querys
|
||||
// to only react to remote syncs. By default, queries will
|
||||
// always update to all changes.
|
||||
if (this._supportedSyncTypes.has(type)) {
|
||||
this.onUpdate(tables);
|
||||
if (
|
||||
(event.type === 'applied' || event.type === 'success') &&
|
||||
this._supportedSyncTypes.has(event.type)
|
||||
) {
|
||||
this.onUpdate(event.tables);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,16 +8,14 @@ import type { Notification } from './state-types/notifications';
|
||||
export function listenForSyncEvent(actions, store) {
|
||||
let attemptedSyncRepair = false;
|
||||
|
||||
listen('sync-event', info => {
|
||||
const { type, subtype, meta, tables } = info;
|
||||
|
||||
listen('sync-event', event => {
|
||||
const prefs = store.getState().prefs.local;
|
||||
if (!prefs || !prefs.id) {
|
||||
// Do nothing if no budget is loaded
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'success') {
|
||||
if (event.type === 'success') {
|
||||
if (attemptedSyncRepair) {
|
||||
attemptedSyncRepair = false;
|
||||
|
||||
@@ -28,6 +26,8 @@ export function listenForSyncEvent(actions, store) {
|
||||
});
|
||||
}
|
||||
|
||||
const tables = event.tables;
|
||||
|
||||
if (tables.includes('prefs')) {
|
||||
actions.loadPrefs();
|
||||
}
|
||||
@@ -47,13 +47,13 @@ export function listenForSyncEvent(actions, store) {
|
||||
if (tables.includes('accounts')) {
|
||||
actions.getAccounts();
|
||||
}
|
||||
} else if (type === 'error') {
|
||||
} else if (event.type === 'error') {
|
||||
let notif: Notification | null = null;
|
||||
const learnMore = `[${t('Learn more')}](https://actualbudget.org/docs/getting-started/sync/#debugging-sync-issues)`;
|
||||
const githubIssueLink =
|
||||
'https://github.com/actualbudget/actual/issues/new?assignees=&labels=bug&template=bug-report.yml&title=%5BBug%5D%3A+';
|
||||
|
||||
switch (subtype) {
|
||||
switch (event.subtype) {
|
||||
case 'out-of-sync':
|
||||
if (attemptedSyncRepair) {
|
||||
notif = {
|
||||
@@ -215,7 +215,7 @@ export function listenForSyncEvent(actions, store) {
|
||||
break;
|
||||
case 'encrypt-failure':
|
||||
case 'decrypt-failure':
|
||||
if (meta.isMissingKey) {
|
||||
if (event.meta.isMissingKey) {
|
||||
notif = {
|
||||
title: t('Missing encryption key'),
|
||||
message: t(
|
||||
@@ -252,7 +252,7 @@ export function listenForSyncEvent(actions, store) {
|
||||
}
|
||||
break;
|
||||
case 'invalid-schema':
|
||||
console.trace('invalid-schema', meta);
|
||||
console.trace('invalid-schema', event.meta);
|
||||
notif = {
|
||||
title: t('Update required'),
|
||||
message: t(
|
||||
@@ -263,7 +263,7 @@ export function listenForSyncEvent(actions, store) {
|
||||
};
|
||||
break;
|
||||
case 'apply-failure':
|
||||
console.trace('apply-failure', meta);
|
||||
console.trace('apply-failure', event.meta);
|
||||
notif = {
|
||||
message: t(
|
||||
'We couldn’t apply that change to the database. Please report this as a bug by [opening a Github issue]({{githubIssueLink}}).',
|
||||
@@ -287,7 +287,7 @@ export function listenForSyncEvent(actions, store) {
|
||||
};
|
||||
break;
|
||||
default:
|
||||
console.trace('unknown error', info);
|
||||
console.trace('unknown error', event);
|
||||
notif = {
|
||||
message: t(
|
||||
'We had problems syncing your changes. Please report this as a bug by [opening a Github issue]({{githubIssueLink}}).',
|
||||
|
||||
@@ -9,7 +9,7 @@ export type Init = typeof init;
|
||||
|
||||
export function send<K extends keyof ServerEvents>(
|
||||
type: K,
|
||||
args?: ServerEvents[k],
|
||||
args?: ServerEvents[K],
|
||||
): void;
|
||||
export type Send = typeof send;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @ts-strict-ignore
|
||||
import * as connection from '../../platform/server/connection';
|
||||
import { Diff } from '../../shared/util';
|
||||
import { TransactionEntity } from '../../types/models';
|
||||
import { PayeeEntity, TransactionEntity } from '../../types/models';
|
||||
import * as db from '../db';
|
||||
import { incrFetch, whereIn } from '../db/util';
|
||||
import { batchMessages } from '../sync';
|
||||
@@ -55,7 +55,7 @@ export async function batchUpdateTransactions({
|
||||
? await idsWithChildren(deleted.map(d => d.id))
|
||||
: [];
|
||||
|
||||
const oldPayees = new Set();
|
||||
const oldPayees = new Set<PayeeEntity['id']>();
|
||||
const accounts = await db.all('SELECT * FROM accounts WHERE tombstone = 0');
|
||||
|
||||
// We need to get all the payees of updated transactions _before_
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
// @ts-strict-ignore
|
||||
import mitt from 'mitt';
|
||||
import mitt, { type Emitter } from 'mitt';
|
||||
|
||||
import { captureException } from '../platform/exceptions';
|
||||
import { ServerEvents } from '../types/server-events';
|
||||
|
||||
// This is a simple helper abstraction for defining methods exposed to
|
||||
// the client. It doesn't do much, but checks for naming conflicts and
|
||||
// makes it cleaner to combine methods. We call a group of related
|
||||
// methods an "app".
|
||||
|
||||
type Events = {
|
||||
sync: ServerEvents['sync-event'];
|
||||
'load-budget': { id: string };
|
||||
};
|
||||
|
||||
class App<Handlers> {
|
||||
events;
|
||||
events: Emitter<Events>;
|
||||
handlers: Handlers;
|
||||
services;
|
||||
unlistenServices;
|
||||
|
||||
@@ -6,6 +6,6 @@ import { createApp } from './app';
|
||||
// Main app
|
||||
export const app = createApp<Handlers>();
|
||||
|
||||
app.events.on('sync', info => {
|
||||
connection.send('sync-event', info);
|
||||
app.events.on('sync', event => {
|
||||
connection.send('sync-event', event);
|
||||
});
|
||||
|
||||
@@ -533,7 +533,7 @@ async function advanceSchedulesService(syncSuccess) {
|
||||
}
|
||||
|
||||
if (failedToPost.length > 0) {
|
||||
connection.send('schedules-offline', { payees: failedToPost });
|
||||
connection.send('schedules-offline');
|
||||
} else if (didPost) {
|
||||
// This forces a full refresh of transactions because it
|
||||
// simulates them coming in from a full sync. This not a
|
||||
@@ -542,7 +542,7 @@ async function advanceSchedulesService(syncSuccess) {
|
||||
connection.send('sync-event', {
|
||||
type: 'success',
|
||||
tables: ['transactions'],
|
||||
syncDisabled: 'false',
|
||||
syncDisabled: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
97
packages/loot-core/src/types/server-events.d.ts
vendored
97
packages/loot-core/src/types/server-events.d.ts
vendored
@@ -1,23 +1,86 @@
|
||||
import { type Backup } from '../server/backups';
|
||||
import { type UndoState } from '../server/undo';
|
||||
|
||||
type SyncSubtype =
|
||||
| 'out-of-sync'
|
||||
| 'apply-failure'
|
||||
| 'decrypt-failure'
|
||||
| 'encrypt-failure'
|
||||
| 'invalid-schema'
|
||||
| 'network'
|
||||
| 'file-old-version'
|
||||
| 'file-key-mismatch'
|
||||
| 'file-not-found'
|
||||
| 'file-needs-upload'
|
||||
| 'file-has-reset'
|
||||
| 'file-has-new-key'
|
||||
| 'token-expired'
|
||||
| string;
|
||||
|
||||
type SyncEvent = {
|
||||
meta?: Record<string, unknown>;
|
||||
} & (
|
||||
| {
|
||||
type: 'applied';
|
||||
tables: string[];
|
||||
data?: Map<string, unknown>;
|
||||
prevData?: Map<string, unknown>;
|
||||
}
|
||||
| {
|
||||
type: 'success';
|
||||
tables: string[];
|
||||
syncDisabled?: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'error';
|
||||
subtype?: SyncSubtype;
|
||||
}
|
||||
| {
|
||||
type: 'start';
|
||||
}
|
||||
| {
|
||||
type: 'unauthorized';
|
||||
}
|
||||
);
|
||||
|
||||
type BackupUpdatedEvent = Backup[];
|
||||
|
||||
type CellsChangedEvent = Array<{
|
||||
name: string;
|
||||
value: string | number | boolean;
|
||||
}>;
|
||||
|
||||
type FallbackWriteErrorEvent = undefined;
|
||||
type FinishImportEvent = undefined;
|
||||
type FinishLoadEvent = undefined;
|
||||
|
||||
type OrphanedPayeesEvent = {
|
||||
orphanedIds: string[];
|
||||
updatedPayeeIds: string[];
|
||||
};
|
||||
|
||||
type PrefsUpdatedEvent = undefined;
|
||||
type SchedulesOfflineEvent = undefined;
|
||||
type ServerErrorEvent = undefined;
|
||||
type ShowBudgetsEvent = undefined;
|
||||
type StartImportEvent = { budgetName: string };
|
||||
type StartLoadEvent = undefined;
|
||||
type ApiFetchRedirectedEvent = undefined;
|
||||
|
||||
export interface ServerEvents {
|
||||
'backups-updated': Backup[];
|
||||
'cells-changed': Array<{ name }>;
|
||||
'fallback-write-error': unknown;
|
||||
'finish-import': unknown;
|
||||
'finish-load': unknown;
|
||||
'orphaned-payees': {
|
||||
orphanedIds: string[];
|
||||
updatedPayeeIds: string[];
|
||||
};
|
||||
'prefs-updated': unknown;
|
||||
'schedules-offline': { payees: unknown[] };
|
||||
'server-error': unknown;
|
||||
'show-budgets': unknown;
|
||||
'start-import': unknown;
|
||||
'start-load': unknown;
|
||||
'sync-event': { type; subtype; meta; tables; syncDisabled };
|
||||
'backups-updated': BackupUpdatedEvent;
|
||||
'cells-changed': CellsChangedEvent;
|
||||
'fallback-write-error': FallbackWriteErrorEvent;
|
||||
'finish-import': FinishImportEvent;
|
||||
'finish-load': FinishLoadEvent;
|
||||
'orphaned-payees': OrphanedPayeesEvent;
|
||||
'prefs-updated': PrefsUpdatedEvent;
|
||||
'schedules-offline': SchedulesOfflineEvent;
|
||||
'server-error': ServerErrorEvent;
|
||||
'show-budgets': ShowBudgetsEvent;
|
||||
'start-import': StartImportEvent;
|
||||
'start-load': StartLoadEvent;
|
||||
'sync-event': SyncEvent;
|
||||
'undo-event': UndoState;
|
||||
'api-fetch-redirected': unknown;
|
||||
'api-fetch-redirected': ApiFetchRedirectedEvent;
|
||||
}
|
||||
|
||||
6
upcoming-release-notes/4110.md
Normal file
6
upcoming-release-notes/4110.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
Add types to loot-core server events.
|
||||
Reference in New Issue
Block a user