Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
69 changes: 69 additions & 0 deletions src/apps/main/database/initialize-virtual-drive-sqlite.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { calls } from 'tests/vitest/utils.helper';
import { initializeVirtualDriveSqlite } from './initialize-virtual-drive-sqlite';

describe('initialize-virtual-drive-sqlite', () => {
const queryMock = vi.fn<(statement: string) => Promise<unknown>>();
let props: NonNullable<Parameters<typeof initializeVirtualDriveSqlite>[0]>;

beforeEach(() => {
queryMock.mockResolvedValue(undefined);
props = {
dataSource: {
isInitialized: true,
query: queryMock,
},
};
});

it('should skip the bootstrap when the data source is not initialized', async () => {
props.dataSource!.isInitialized = false;

await initializeVirtualDriveSqlite(props);

calls(queryMock).toHaveLength(0);
});

it('should configure sqlite and create the directory state schema', async () => {
await initializeVirtualDriveSqlite(props);

calls(queryMock).toHaveLength(11);
expect(queryMock.mock.invocationCallOrder).toStrictEqual(
[...queryMock.mock.invocationCallOrder].sort((a, b) => a - b),
);
expect(queryMock).toHaveBeenCalledWith('PRAGMA journal_mode=WAL;');
expect(queryMock).not.toHaveBeenCalledWith('PRAGMA synchronous=NORMAL;');
expect(queryMock).toHaveBeenCalledWith(expect.stringContaining('CREATE TABLE IF NOT EXISTS drive_directory_state'));
expect(queryMock.mock.calls[0]).toStrictEqual(['PRAGMA busy_timeout=5000;']);
expect(queryMock.mock.calls[1]).toStrictEqual(['PRAGMA journal_mode=WAL;']);
});

it('should propagate WAL errors', async () => {
const error = new Error('database is unavailable');
queryMock.mockImplementation((statement) => {
if (statement === 'PRAGMA journal_mode=WAL;') {
return Promise.reject(error);
}

return Promise.resolve(undefined);
});

await expect(initializeVirtualDriveSqlite(props)).rejects.toBe(error);

calls(queryMock).toHaveLength(2);
});

it('should propagate directory state schema errors', async () => {
const error = new Error('schema creation failed');
queryMock.mockImplementation((statement) => {
if (statement.includes('CREATE TABLE IF NOT EXISTS drive_directory_state')) {
return Promise.reject(error);
}

return Promise.resolve(undefined);
});

await expect(initializeVirtualDriveSqlite(props)).rejects.toBe(error);

expect(queryMock).toHaveBeenCalledWith(expect.stringContaining('CREATE TABLE IF NOT EXISTS drive_directory_state'));
});
});
41 changes: 41 additions & 0 deletions src/apps/main/database/initialize-virtual-drive-sqlite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { AppDataSource } from './data-source';

type SqliteQueryExecutor = {
isInitialized: boolean;
query: (statement: string) => Promise<unknown>;
};

type Props = {
dataSource?: SqliteQueryExecutor;
};

const SQLITE_BOOTSTRAP_STATEMENTS = [
'PRAGMA busy_timeout=5000;',
'PRAGMA journal_mode=WAL;',
Comment thread
egalvis27 marked this conversation as resolved.
'PRAGMA temp_store=MEMORY;',
'PRAGMA foreign_keys=ON;',
'PRAGMA wal_autocheckpoint=1000;',
'PRAGMA mmap_size=268435456;',
`CREATE TABLE IF NOT EXISTS drive_directory_state (
folder_id INTEGER NOT NULL,
status_scope TEXT NOT NULL,
children_loaded_at TEXT NULL,
children_last_error_at TEXT NULL,
fetch_state TEXT NOT NULL DEFAULT 'idle',
PRIMARY KEY (folder_id, status_scope)
);`,
'CREATE INDEX IF NOT EXISTS idx_drive_file_folder_status_updated ON drive_file(folderId, status, updatedAt);',
'CREATE INDEX IF NOT EXISTS idx_drive_file_status_updated ON drive_file(status, updatedAt);',
'CREATE INDEX IF NOT EXISTS idx_drive_folder_parent_status_updated ON drive_folder(parentId, status, updatedAt);',
'CREATE INDEX IF NOT EXISTS idx_drive_folder_status_updated ON drive_folder(status, updatedAt);',
];

export async function initializeVirtualDriveSqlite({ dataSource = AppDataSource }: Props = {}) {
if (!dataSource.isInitialized) {
return;
}

for (const statement of SQLITE_BOOTSTRAP_STATEMENTS) {
await dataSource.query(statement);

Check warning on line 39 in src/apps/main/database/initialize-virtual-drive-sqlite.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected `await` inside a loop.

See more on https://sonarcloud.io/project/issues?id=internxt_drive-desktop-linux&issues=AaEiXgUgngGJCBtb-LE-&open=AaEiXgUgngGJCBtb-LE-&pullRequest=468
}
}
9 changes: 4 additions & 5 deletions src/backend/features/auth/get-user-or-throw.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { getUser } from './get-user';


export function getUserOrThrow() {
const { data: user, error } = getUser();
if (error) throw error;
const { data: user, error } = getUser();
if (error) throw error;

return user;
}
return user;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DIRECTORY_STATE_TTL_MS = 30_000;
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { call } from 'tests/vitest/utils.helper';
import { DirectoryStateRepository } from './directory-state-sqlite-repository';

describe('directory-state-sqlite-repository', () => {
const queryMock =
vi.fn<(statement: string, parameters?: Array<unknown>) => Promise<Array<{ children_loaded_at: string | null }>>>();
const stateProps = { folderId: 42, statusScope: 'EXISTS' as const };
const dataSource = { query: queryMock };

beforeEach(() => {
queryMock.mockResolvedValue([]);
});

it('should report a directory as stale when it has not been loaded', async () => {
const isFresh = await DirectoryStateRepository.isFresh({ dataSource, ...stateProps });

expect(isFresh).toBe(false);
call(queryMock).toStrictEqual([
'SELECT children_loaded_at FROM drive_directory_state WHERE folder_id = ? AND status_scope = ? LIMIT 1',
[42, 'EXISTS'],
]);
});

it('should report a directory as fresh when its state is within the ttl', async () => {
queryMock.mockResolvedValue([{ children_loaded_at: new Date().toISOString() }]);

const isFresh = await DirectoryStateRepository.isFresh({ dataSource, ...stateProps, ttlMs: 30_000 });

expect(isFresh).toBe(true);
});

it('should report a directory as stale when its state exceeds the ttl', async () => {
queryMock.mockResolvedValue([{ children_loaded_at: new Date(Date.now() - 30_001).toISOString() }]);

const isFresh = await DirectoryStateRepository.isFresh({ dataSource, ...stateProps, ttlMs: 30_000 });

expect(isFresh).toBe(false);
});

it('should record a successful directory load', async () => {
await DirectoryStateRepository.markLoaded({ dataSource, ...stateProps });

expect(queryMock).toHaveBeenCalledWith(
expect.stringContaining('DO UPDATE SET children_loaded_at = excluded.children_loaded_at'),
[42, 'EXISTS', expect.any(String)],
);
});

it('should record a directory load error', async () => {
await DirectoryStateRepository.markError({ dataSource, ...stateProps });

expect(queryMock).toHaveBeenCalledWith(expect.stringContaining('VALUES (?, ?, NULL, ?,'), [
42,
'EXISTS',
expect.any(String),
]);
});

it('should clear all directory states', async () => {
await DirectoryStateRepository.clear({ dataSource });

call(queryMock).toStrictEqual('DELETE FROM drive_directory_state');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { AppDataSource } from '../../../../../apps/main/database/data-source';
import { DIRECTORY_STATE_TTL_MS } from './constants';
import type {
DirectoryStateFreshnessProps,
DirectoryStateOperationProps,
DirectoryStateRepositoryProps,
} from './types';

async function isFresh({
dataSource = AppDataSource,
folderId,
statusScope,
ttlMs = DIRECTORY_STATE_TTL_MS,
}: DirectoryStateFreshnessProps) {
const rows = await dataSource.query(
'SELECT children_loaded_at FROM drive_directory_state WHERE folder_id = ? AND status_scope = ? LIMIT 1',
[folderId, statusScope],
);
const loadedAt = rows[0]?.children_loaded_at;

if (!loadedAt) {
return false;
}

return Date.now() - new Date(loadedAt).getTime() < ttlMs;
}

async function markLoaded({ dataSource = AppDataSource, folderId, statusScope }: DirectoryStateOperationProps) {
await dataSource.query(
`INSERT INTO drive_directory_state (folder_id, status_scope, children_loaded_at, children_last_error_at, fetch_state)
VALUES (?, ?, ?, NULL, 'idle')
ON CONFLICT(folder_id, status_scope)
DO UPDATE SET children_loaded_at = excluded.children_loaded_at, children_last_error_at = NULL, fetch_state = 'idle'`,
[folderId, statusScope, new Date().toISOString()],
);
}

async function markError({ dataSource = AppDataSource, folderId, statusScope }: DirectoryStateOperationProps) {
await dataSource.query(
`INSERT INTO drive_directory_state (folder_id, status_scope, children_loaded_at, children_last_error_at, fetch_state)
VALUES (?, ?, NULL, ?, 'error')
ON CONFLICT(folder_id, status_scope)
DO UPDATE SET children_last_error_at = excluded.children_last_error_at, fetch_state = 'error'`,
[folderId, statusScope, new Date().toISOString()],
);
}

async function clear({ dataSource = AppDataSource }: DirectoryStateRepositoryProps = {}) {
await dataSource.query('DELETE FROM drive_directory_state');
}

export const DirectoryStateRepository = {
isFresh,
markLoaded,
markError,
clear,
};
24 changes: 24 additions & 0 deletions src/backend/features/virtual-drive/services/lazy/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export type DirectoryStatusScope = 'EXISTS';

export type DirectoryStateRow = {
children_loaded_at: string | null;
};

export type SqliteQueryExecutor = {
query: (statement: string, parameters?: Array<unknown>) => Promise<Array<DirectoryStateRow>>;
};

export type DirectoryStateProps = {
folderId: number;
statusScope: DirectoryStatusScope;
};

export type DirectoryStateRepositoryProps = {
dataSource?: SqliteQueryExecutor;
};

export type DirectoryStateOperationProps = DirectoryStateProps & DirectoryStateRepositoryProps;

export type DirectoryStateFreshnessProps = DirectoryStateOperationProps & {
ttlMs?: number;
};
30 changes: 30 additions & 0 deletions src/core/bootstrap/register-session-event-handlers.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as eventBusModule from '../../apps/main/event-bus';
import * as appDataSourceModule from '../../apps/main/database/data-source';
import * as virtualDriveSqliteModule from '../../apps/main/database/initialize-virtual-drive-sqlite';
import * as widgetModule from '../../apps/main/windows/widget';
import * as authWindowModule from '../../apps/main/windows/auth';
import * as configStoreModule from '../../apps/main/config';
Expand Down Expand Up @@ -28,6 +29,10 @@ vi.mock('../../apps/main/database/data-source', () => ({
},
}));

vi.mock('../../apps/main/database/initialize-virtual-drive-sqlite', () => ({
initializeVirtualDriveSqlite: vi.fn(),
}));

vi.mock('../../apps/main/windows/widget', () => ({
getOrCreateWidged: vi.fn(),
setBoundsOfWidgetByPath: vi.fn(),
Expand Down Expand Up @@ -84,7 +89,10 @@ import { registerSessionEventHandlers } from './register-session-event-handlers'

describe('register-session-event-handlers', () => {
const eventBusOnSpy = partialSpyOn(eventBusModule.default, 'on');
const eventBusEmitSpy = partialSpyOn(eventBusModule.default, 'emit');
const appDataSourceInitializeSpy = partialSpyOn(appDataSourceModule.AppDataSource, 'initialize');
const appDataSourceIsInitializedSpy = vi.spyOn(appDataSourceModule.AppDataSource, 'isInitialized', 'get');
const initializeVirtualDriveSqliteSpy = partialSpyOn(virtualDriveSqliteModule, 'initializeVirtualDriveSqlite');
const getOrCreateWidgedSpy = partialSpyOn(widgetModule, 'getOrCreateWidged');
const getAuthWindowSpy = partialSpyOn(authWindowModule, 'getAuthWindow');
const configStoreGetSpy = partialSpyOn(configStoreModule.default, 'get');
Expand All @@ -101,7 +109,9 @@ describe('register-session-event-handlers', () => {

beforeEach(() => {
eventBusOnSpy.mockImplementation(() => ({}) as never);
appDataSourceIsInitializedSpy.mockReturnValue(false);
appDataSourceInitializeSpy.mockResolvedValue({} as never);
initializeVirtualDriveSqliteSpy.mockResolvedValue(undefined);
getOrCreateWidgedSpy.mockResolvedValue({ show: vi.fn() } as never);
getAuthWindowSpy.mockReturnValue({ hide: vi.fn(), destroy: vi.fn(), isDestroyed: () => false } as never);
configStoreGetSpy.mockReturnValue(undefined);
Expand Down Expand Up @@ -132,11 +142,31 @@ describe('register-session-event-handlers', () => {
await (loginHandler as (...args: unknown[]) => Promise<void> | void)();

expect(appDataSourceInitializeSpy).toHaveBeenCalledTimes(1);
expect(initializeVirtualDriveSqliteSpy).toHaveBeenCalledTimes(1);
expect(appDataSourceInitializeSpy.mock.invocationCallOrder[0]).toBeLessThan(
initializeVirtualDriveSqliteSpy.mock.invocationCallOrder[0],
);
expect(getUserAvailableProductsAndStoreSpy).toHaveBeenCalledTimes(1);
expect(getThemeSpy).toHaveBeenCalledTimes(1);
expect(resetTrayStatusSpy).toHaveBeenCalledWith('IDLE');
expect(openOnboardingWindowSpy).toHaveBeenCalledTimes(1);
expect(trySetupAntivirusSpy).toHaveBeenCalledTimes(1);
expect(showMarketingNotificationsSpy).toHaveBeenCalledTimes(1);
});

it('should retry sqlite bootstrap after it fails on a previous login attempt', async () => {
const error = new Error('sqlite bootstrap failed');
initializeVirtualDriveSqliteSpy.mockRejectedValueOnce(error).mockResolvedValueOnce(undefined);
appDataSourceIsInitializedSpy.mockReturnValueOnce(false).mockReturnValue(true);
registerSessionEventHandlers();

const [, loginHandler] = eventBusOnSpy.mock.calls[1];
await (loginHandler as (...args: unknown[]) => Promise<void> | void)();
await (loginHandler as (...args: unknown[]) => Promise<void> | void)();

expect(appDataSourceInitializeSpy).toHaveBeenCalledTimes(1);
expect(initializeVirtualDriveSqliteSpy).toHaveBeenCalledTimes(2);
expect(eventBusEmitSpy).toHaveBeenCalledWith('APP_DATA_SOURCE_INITIALIZED');
expect(eventBusEmitSpy).toHaveBeenCalledTimes(1);
});
});
23 changes: 19 additions & 4 deletions src/core/bootstrap/register-session-event-handlers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { logger } from '@internxt/drive-desktop-core/build/backend';
import eventBus from '../../apps/main/event-bus';
import { AppDataSource } from '../../apps/main/database/data-source';
import { initializeVirtualDriveSqlite } from '../../apps/main/database/initialize-virtual-drive-sqlite';
import { getOrCreateWidged, setBoundsOfWidgetByPath } from '../../apps/main/windows/widget';
import { getAuthWindow } from '../../apps/main/windows/auth';
import configStore from '../../apps/main/config';
Expand All @@ -14,17 +15,31 @@ import { startBackupsIfAvailable } from '../../backend/features/backup/start-bac
import { resolveUserFileSizeLimit } from '../../backend/features/user/file-size-limit/resolve-user-file-size-limit';
import { showMarketingNotifications } from '../../backend/features/marketing';

let isVirtualDriveSqliteInitialized = false;

function onWidgetIsReady() {
registerBackupHandlers();
startBackupsIfAvailable();
}

async function initializeAppDataSource() {
if (!AppDataSource.isInitialized) {
isVirtualDriveSqliteInitialized = false;
await AppDataSource.initialize();
}

if (isVirtualDriveSqliteInitialized) {
return;
}

await initializeVirtualDriveSqlite();
isVirtualDriveSqliteInitialized = true;
eventBus.emit('APP_DATA_SOURCE_INITIALIZED');
}

async function onUserLoggedIn() {
try {
if (!AppDataSource.isInitialized) {
await AppDataSource.initialize();
eventBus.emit('APP_DATA_SOURCE_INITIALIZED');
}
await initializeAppDataSource();

// Keep product flags in sync even if later UI steps fail.
await getUserAvailableProductsAndStore();
Expand Down
Loading