Files
iptvnator/apps/electron-backend/src/app/services/database-worker-client.spec.ts
T
4gray 456173b772 feat(database): implement DatabaseWorkerClient and associated worker functionality
- Added DatabaseWorkerClient to manage database operations in a worker thread.
- Created database-worker-client.ts to handle requests and responses between the main process and the worker.
- Introduced database-worker.types.ts for type definitions related to worker messages and operations.
- Implemented database.worker.ts to execute various database operations, including CRUD operations for playlists, content, and favorites.
- Added database.worker-connection.ts to manage database connections and ensure proper handling of SQLite instances.
- Created tests for DatabaseWorkerClient to ensure correct request handling and error management.
- Updated xtream.events.ts to allow suppression of error logging for specific requests.
- Enhanced EPG parser worker to include a busy timeout for SQLite operations.
- Integrated shutdown logic for the DatabaseWorkerClient in the main application lifecycle.
2026-03-28 15:40:06 +01:00

247 lines
7.3 KiB
TypeScript

import type { DatabaseWorkerClient as DatabaseWorkerClientType } from './database-worker-client';
const mockWorkerInstances: any[] = [];
jest.mock('electron', () => ({
app: {
isPackaged: false,
getAppPath: () => '/mock/app.asar',
},
}));
jest.mock('worker_threads', () => {
const { EventEmitter } = require('events');
class MockWorker extends EventEmitter {
postMessage = jest.fn();
terminate = jest.fn().mockResolvedValue(0);
}
return {
Worker: jest.fn().mockImplementation(() => {
const worker = new MockWorker();
mockWorkerInstances.push(worker);
return worker;
}),
};
});
describe('DatabaseWorkerClient', () => {
let DatabaseWorkerClient: typeof DatabaseWorkerClientType;
let clients: DatabaseWorkerClientType[];
beforeEach(async () => {
jest.resetModules();
mockWorkerInstances.length = 0;
clients = [];
({ DatabaseWorkerClient } = await import('./database-worker-client'));
});
afterEach(async () => {
await Promise.all(
clients.map((client) => client.shutdown().catch(() => undefined))
);
});
function createClient(): DatabaseWorkerClientType {
const client = new DatabaseWorkerClient();
clients.push(client);
return client;
}
async function flushPromises(): Promise<void> {
await new Promise((resolve) => setImmediate(resolve));
}
it('resolves requests after the worker reports ready and returns a response', async () => {
const client = createClient();
const requestPromise = client.request('DB_GLOBAL_SEARCH', {
searchTerm: 'matrix',
});
const worker = mockWorkerInstances[0];
worker.emit('message', { type: 'ready' });
await flushPromises();
expect(worker.postMessage).toHaveBeenCalledTimes(1);
const request = worker.postMessage.mock.calls[0][0];
worker.emit('message', {
type: 'response',
requestId: request.requestId,
success: true,
result: [{ id: 1 }],
});
await expect(requestPromise).resolves.toEqual([{ id: 1 }]);
});
it('forwards worker events to the pending request callback', async () => {
const client = createClient();
const onEvent = jest.fn();
const requestPromise = client.request(
'DB_SAVE_CONTENT',
{ playlistId: 'xtream-1', streams: [], type: 'movie' },
{ onEvent }
);
const worker = mockWorkerInstances[0];
worker.emit('message', { type: 'ready' });
await flushPromises();
const request = worker.postMessage.mock.calls[0][0];
const event = {
operationId: 'op-1',
operation: 'save-content',
status: 'progress',
current: 10,
total: 100,
};
worker.emit('message', {
type: 'event',
requestId: request.requestId,
event,
});
expect(onEvent).toHaveBeenCalledWith(event);
worker.emit('message', {
type: 'response',
requestId: request.requestId,
success: true,
result: { success: true, count: 10 },
});
await expect(requestPromise).resolves.toEqual({
success: true,
count: 10,
});
});
it('turns serialized worker errors into rejected Error instances', async () => {
const client = createClient();
const requestPromise = client.request('DB_DELETE_PLAYLIST', {
playlistId: 'xtream-1',
});
const worker = mockWorkerInstances[0];
worker.emit('message', { type: 'ready' });
await flushPromises();
const request = worker.postMessage.mock.calls[0][0];
worker.emit('message', {
type: 'response',
requestId: request.requestId,
success: false,
error: {
name: 'SqliteBusyError',
message: 'database is locked',
stack: 'stack-trace',
},
});
await expect(requestPromise).rejects.toMatchObject({
name: 'SqliteBusyError',
message: 'database is locked',
});
});
it('preserves AbortError responses from the worker', async () => {
const client = createClient();
const requestPromise = client.request('DB_SAVE_CONTENT', {
playlistId: 'xtream-1',
streams: [],
type: 'movie',
});
const worker = mockWorkerInstances[0];
worker.emit('message', { type: 'ready' });
await flushPromises();
const request = worker.postMessage.mock.calls[0][0];
worker.emit('message', {
type: 'response',
requestId: request.requestId,
success: false,
error: {
name: 'AbortError',
message: 'Operation "save-content" was cancelled',
},
});
await expect(requestPromise).rejects.toMatchObject({
name: 'AbortError',
message: 'Operation "save-content" was cancelled',
});
});
it('posts cancel messages to the current worker', async () => {
const client = createClient();
const requestPromise = client.request('DB_DELETE_PLAYLIST', {
playlistId: 'xtream-1',
});
const worker = mockWorkerInstances[0];
worker.emit('message', { type: 'ready' });
await flushPromises();
await expect(client.cancel('operation-123')).resolves.toEqual({
success: true,
});
expect(worker.postMessage).toHaveBeenCalledWith({
type: 'cancel',
operationId: 'operation-123',
});
const request = worker.postMessage.mock.calls[0][0];
worker.emit('message', {
type: 'response',
requestId: request.requestId,
success: true,
result: { success: true },
});
await expect(requestPromise).resolves.toEqual({ success: true });
});
it('rejects pending work on worker exit and starts a fresh worker for the next request', async () => {
const client = createClient();
const firstRequest = client.request('DB_SEARCH_CONTENT', {
playlistId: 'xtream-1',
searchTerm: 'matrix',
types: ['movie'],
});
const firstWorker = mockWorkerInstances[0];
firstWorker.emit('message', { type: 'ready' });
await flushPromises();
firstWorker.emit('exit', 1);
await expect(firstRequest).rejects.toThrow(
'Database worker stopped with exit code 1'
);
const secondRequest = client.request('DB_SEARCH_CONTENT', {
playlistId: 'xtream-1',
searchTerm: 'neo',
types: ['movie'],
});
const secondWorker = mockWorkerInstances[1];
secondWorker.emit('message', { type: 'ready' });
await flushPromises();
const request = secondWorker.postMessage.mock.calls[0][0];
secondWorker.emit('message', {
type: 'response',
requestId: request.requestId,
success: true,
result: [],
});
await expect(secondRequest).resolves.toEqual([]);
});
});