559 lines
18 KiB
Markdown
559 lines
18 KiB
Markdown
# SQLite DB Worker
|
|
|
|
This document records the current non-EPG SQLite worker implementation in the
|
|
Electron app.
|
|
|
|
Related:
|
|
|
|
- [Category Management](./category-management.md)
|
|
- [Workspace Shell](./workspace-shell.md)
|
|
|
|
## Summary
|
|
|
|
- Heavy non-EPG SQLite work no longer runs on Electron's main thread.
|
|
- A dedicated long-lived database worker now handles the slow Xtream and
|
|
playlist database operations that were freezing the UI.
|
|
- Renderer APIs stay stable. The main change is that progress and long-running
|
|
state now flow through a request-scoped `DB_OPERATION_EVENT` contract instead
|
|
of a single global progress event.
|
|
|
|
## Goals
|
|
|
|
The worker cutover addresses three concrete problems:
|
|
|
|
1. Main-process UI stalls during large SQLite operations.
|
|
2. Xtream import progress events were global and unsafe for concurrent jobs.
|
|
3. EPG and non-EPG writers needed shared SQLite concurrency settings so they
|
|
can coexist without `SQLITE_BUSY` regressions.
|
|
|
|
## Current Ownership
|
|
|
|
### Main-process runtime wiring
|
|
|
|
These files own worker lifecycle and IPC bridging:
|
|
|
|
1. `apps/electron-backend/src/app/services/database-worker-client.ts`
|
|
2. `apps/electron-backend/src/app/events/database/category.events.ts`
|
|
3. `apps/electron-backend/src/app/events/database/content.events.ts`
|
|
4. `apps/electron-backend/src/app/events/database/playlist.events.ts`
|
|
5. `apps/electron-backend/src/app/events/database/xtream.events.ts`
|
|
6. `apps/electron-backend/src/main.ts`
|
|
|
|
### Worker runtime
|
|
|
|
These files own the worker protocol and the SQLite work itself:
|
|
|
|
1. `apps/electron-backend/src/app/workers/database-worker.types.ts`
|
|
2. `apps/electron-backend/src/app/workers/database.worker.ts`
|
|
3. `apps/electron-backend/src/app/workers/database.worker-connection.ts`
|
|
4. `apps/electron-backend/src/app/workers/worker-runtime-paths.ts`
|
|
|
|
### Pure database operation modules
|
|
|
|
Keep SQL-heavy logic here so the worker entry remains a thin dispatcher:
|
|
|
|
1. `apps/electron-backend/src/app/database/operations/category.operations.ts`
|
|
2. `apps/electron-backend/src/app/database/operations/content.operations.ts`
|
|
3. `apps/electron-backend/src/app/database/operations/playlist.operations.ts`
|
|
4. `apps/electron-backend/src/app/database/operations/xtream.operations.ts`
|
|
|
|
## Worker Architecture
|
|
|
|
### Request flow
|
|
|
|
1. Renderer calls the existing preload API such as `window.electron.dbSaveContent`.
|
|
2. `ipcMain.handle(...)` in the Electron backend builds a payload and delegates
|
|
to `DatabaseWorkerClient`.
|
|
3. `DatabaseWorkerClient` lazily starts one long-lived `worker_threads` worker
|
|
and correlates requests with a generated `requestId`.
|
|
4. The worker executes SQLite work and sends back either:
|
|
1. `ready`
|
|
2. `event`
|
|
3. `response`
|
|
5. The main process resolves the IPC request and forwards worker events back to
|
|
the originating renderer process.
|
|
|
|
### Why one long-lived worker
|
|
|
|
- It avoids worker startup cost on every search/delete/import.
|
|
- It centralizes failure handling and restart behavior.
|
|
- It mirrors the existing EPG worker approach without multiplying writable
|
|
SQLite owners.
|
|
|
|
### Packaged worker bootstrap
|
|
|
|
Packaged Electron builds do not load worker scripts and native modules from the
|
|
same place:
|
|
|
|
1. worker scripts live under `Resources/dist/apps/electron-backend/workers`
|
|
2. unpacked native modules live under one of the approved
|
|
`app.asar.unpacked/.../node_modules` locations
|
|
|
|
Both the EPG worker and the DB worker now share the same runtime helper:
|
|
|
|
1. `resolveWorkerRuntimeBootstrap(...)` for main-process worker launch
|
|
2. `loadNativeModuleFromSearchPaths(...)` for worker-side native module loading
|
|
|
|
The helper uses `process.resourcesPath` as the primary packaged base and keeps
|
|
`path.dirname(app.getAppPath())` only as a fallback.
|
|
|
|
## Worker Message Contract
|
|
|
|
The worker contract lives in
|
|
`apps/electron-backend/src/app/workers/database-worker.types.ts`.
|
|
|
|
### Core message types
|
|
|
|
1. `DbWorkerRequestMessage`
|
|
2. `DbWorkerResponseMessage`
|
|
3. `DbWorkerEventMessage`
|
|
4. `DbOperationEvent`
|
|
|
|
### Progress event contract
|
|
|
|
The worker now emits request-scoped events with:
|
|
|
|
- `operationId`
|
|
- `operation`
|
|
- `playlistId`
|
|
- `status`
|
|
- optional `phase`
|
|
- optional `current`
|
|
- optional `total`
|
|
- optional `increment`
|
|
|
|
Current shipped operation names:
|
|
|
|
1. `save-content`
|
|
2. `delete-xtream-content`
|
|
3. `restore-xtream-user-data`
|
|
4. `delete-playlist`
|
|
5. `delete-all-playlists`
|
|
|
|
The event is forwarded to the renderer as `DB_OPERATION_EVENT`.
|
|
|
|
### Cancellation contract
|
|
|
|
Long-running Xtream and playlist operations now support best-effort
|
|
cancellation.
|
|
|
|
Renderer requests cancellation via:
|
|
|
|
1. `DB_CANCEL_OPERATION`
|
|
2. `window.electron.dbCancelOperation(operationId)`
|
|
3. `DatabaseService.cancelOperation(operationId)`
|
|
|
|
If a worker operation is canceled:
|
|
|
|
1. the worker emits a final `cancelled` event
|
|
2. the request rejects with an `AbortError`
|
|
3. the UI clears its busy state without treating the operation as success
|
|
|
|
Cancellation is cooperative and chunk-based. Already committed SQLite batches
|
|
stay committed.
|
|
|
|
## Renderer Contract
|
|
|
|
The preload bridge keeps the existing database methods but adds scoped worker
|
|
events.
|
|
|
|
### Important preload APIs
|
|
|
|
1. `onDbOperationEvent(callback)`
|
|
2. `dbSaveContent(playlistId, streams, type, operationId?)`
|
|
3. `dbDeleteXtreamContent(playlistId, operationId?)`
|
|
4. `dbRestoreXtreamUserData(..., operationId?)`
|
|
5. `dbDeletePlaylist(playlistId, operationId?)`
|
|
6. `dbDeleteAllPlaylists(operationId?)`
|
|
7. `dbCancelOperation(operationId)`
|
|
8. legacy compatibility:
|
|
1. `onDbSaveContentProgress(callback)`
|
|
2. `removeDbSaveContentProgress()`
|
|
|
|
`DatabaseService.saveXtreamContent(...)` now generates an `operationId`,
|
|
subscribes to `onDbOperationEvent`, filters by that `operationId`, and only
|
|
falls back to the legacy progress API if the newer event channel is missing.
|
|
|
|
`DatabaseService` also owns:
|
|
|
|
1. `createOperationId(...)`
|
|
2. `cancelOperation(operationId)`
|
|
3. `isDbAbortError(error)`
|
|
|
|
## Migrated Operations
|
|
|
|
The worker now owns all heavy non-EPG SQLite paths plus the remaining portal
|
|
state handlers that still used direct main-thread SQLite access.
|
|
|
|
### Categories
|
|
|
|
1. `DB_HAS_CATEGORIES`
|
|
2. `DB_GET_CATEGORIES`
|
|
3. `DB_SAVE_CATEGORIES`
|
|
4. `DB_GET_ALL_CATEGORIES`
|
|
5. `DB_UPDATE_CATEGORY_VISIBILITY`
|
|
|
|
### Content
|
|
|
|
1. `DB_HAS_CONTENT`
|
|
2. `DB_GET_CONTENT`
|
|
3. `DB_SAVE_CONTENT`
|
|
4. `DB_GET_CONTENT_BY_XTREAM_ID`
|
|
5. `DB_SEARCH_CONTENT`
|
|
6. `DB_GLOBAL_SEARCH`
|
|
7. `DB_GET_GLOBAL_RECENTLY_ADDED`
|
|
|
|
### Playlist metadata
|
|
|
|
1. `DB_CREATE_PLAYLIST`
|
|
2. `DB_UPSERT_APP_PLAYLIST`
|
|
3. `DB_UPSERT_APP_PLAYLISTS`
|
|
4. `DB_GET_APP_PLAYLISTS`
|
|
5. `DB_GET_APP_PLAYLIST`
|
|
6. `DB_GET_PLAYLIST`
|
|
7. `DB_UPDATE_PLAYLIST`
|
|
8. `DB_DELETE_PLAYLIST`
|
|
9. `DB_DELETE_ALL_PLAYLISTS`
|
|
10. `DB_GET_APP_STATE`
|
|
11. `DB_SET_APP_STATE`
|
|
|
|
### Xtream refresh helpers
|
|
|
|
1. `DB_DELETE_XTREAM_CONTENT`
|
|
2. `DB_RESTORE_XTREAM_USER_DATA`
|
|
|
|
### Favorites
|
|
|
|
1. `DB_ADD_FAVORITE`
|
|
2. `DB_REMOVE_FAVORITE`
|
|
3. `DB_IS_FAVORITE`
|
|
4. `DB_GET_FAVORITES`
|
|
5. `DB_GET_GLOBAL_FAVORITES`
|
|
6. `DB_GET_ALL_GLOBAL_FAVORITES`
|
|
7. `DB_REORDER_GLOBAL_FAVORITES`
|
|
|
|
### Recently viewed
|
|
|
|
1. `DB_GET_RECENTLY_VIEWED`
|
|
2. `DB_CLEAR_RECENTLY_VIEWED`
|
|
3. `DB_GET_RECENT_ITEMS`
|
|
4. `DB_ADD_RECENT_ITEM`
|
|
5. `DB_CLEAR_PLAYLIST_RECENT_ITEMS`
|
|
6. `DB_REMOVE_RECENT_ITEM`
|
|
|
|
### Playback positions
|
|
|
|
1. `DB_SAVE_PLAYBACK_POSITION`
|
|
2. `DB_GET_PLAYBACK_POSITION`
|
|
3. `DB_GET_SERIES_PLAYBACK_POSITIONS`
|
|
4. `DB_GET_RECENT_PLAYBACK_POSITIONS`
|
|
5. `DB_GET_ALL_PLAYBACK_POSITIONS`
|
|
6. `DB_CLEAR_PLAYBACK_POSITION`
|
|
|
|
## SQLite Concurrency Rules
|
|
|
|
EPG remains on its own worker, so both workers must use compatible SQLite
|
|
pragmas.
|
|
|
|
Applied now in both the shared connection path and worker-owned connections:
|
|
|
|
1. `foreign_keys = ON`
|
|
2. `journal_mode = WAL`
|
|
3. `busy_timeout = 5000`
|
|
|
|
Current sources:
|
|
|
|
1. `libs/shared/database/src/lib/connection.ts`
|
|
2. `apps/electron-backend/src/app/workers/database.worker-connection.ts`
|
|
3. `apps/electron-backend/src/app/workers/epg-parser.worker.ts`
|
|
|
|
## UI Behavior Changes
|
|
|
|
### Search
|
|
|
|
Xtream search now guards against stale async responses:
|
|
|
|
1. local playlist search uses a monotonically increasing request version
|
|
2. global search uses a separate request version in the dialog component
|
|
3. clearing search invalidates older pending results
|
|
|
|
This prevents an older worker response from repainting over a newer query or a
|
|
cleared search state.
|
|
|
|
### Xtream type-aware content lookup
|
|
|
|
Xtream UI flows must treat `xtream_id` as only partially unique.
|
|
|
|
Current contract:
|
|
|
|
1. `xtream_id` can collide across `live`, `movie`, and `series` within the same
|
|
playlist.
|
|
2. Any DB-backed lookup that starts from an Xtream result card, favorite button,
|
|
recent-item update, continue-watching flow, or detail route must resolve
|
|
content by:
|
|
- `playlist_id`
|
|
- `xtream_id`
|
|
- `content.type`
|
|
3. Mixed Xtream collection identity must key entries by `type + xtream_id`,
|
|
not `xtream_id` alone. This includes favorites maps, recent-item lists,
|
|
dashboard collection payloads, and other UI state keyed off persisted
|
|
Xtream content.
|
|
|
|
Why this matters:
|
|
|
|
- Search results are already type-filtered, so resolving favorites by only
|
|
`playlist_id + xtream_id` can favorite the wrong persisted row when IDs
|
|
collide.
|
|
- Continue-watching / recently-viewed flows that resolve by only
|
|
`playlist_id + xtream_id` can store the wrong persisted row when a series or
|
|
movie ID collides with a live entry.
|
|
- Mixed favorites maps keyed only by `xtream_id` can mark an unrelated live row
|
|
as favorited when the actual favorite is a movie or series with the same
|
|
numeric ID.
|
|
- Mixed recent/favorites collection items keyed only by `xtream_id` can cause
|
|
local UI state to remove, reorder, or reactivate the wrong Xtream entry when
|
|
different content types collide.
|
|
|
|
Current implementation paths:
|
|
|
|
1. `apps/electron-backend/src/app/database/operations/content.operations.ts`
|
|
2. `libs/portal/xtream/data-access/src/lib/with-favorites.feature.ts`
|
|
3. `libs/portal/xtream/data-access/src/lib/with-recent-items.ts`
|
|
4. `libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.ts`
|
|
5. `libs/portal/shared/util/src/lib/collection/unified-recent-data.service.ts`
|
|
6. `libs/portal/shared/util/src/lib/collection/unified-favorites-data.service.ts`
|
|
|
|
### Busy states
|
|
|
|
The UI now has explicit long-running state for destructive operations:
|
|
|
|
1. recent playlist rows show row-level refresh/delete spinners
|
|
2. Xtream import overlay shows phase text and a cancel action
|
|
3. Xtream playlist rows show request-scoped progress and cancel actions
|
|
4. busy rows block repeat clicks while an operation is in flight
|
|
5. settings "remove all playlists" owns its own spinner/disabled state and
|
|
consumes request-scoped DB operation events for progress text while the
|
|
worker deletes playlist data
|
|
|
|
These changes matter because once SQLite work leaves the main thread, the
|
|
renderer can actually paint the loading state instead of freezing.
|
|
|
|
## Build And Packaging Notes
|
|
|
|
### Worker bundling
|
|
|
|
`apps/electron-backend/build-worker.js` now bundles both:
|
|
|
|
1. `epg-parser.worker.ts`
|
|
2. `database.worker.ts`
|
|
|
|
The worker build also aliases:
|
|
|
|
1. `database-schema`
|
|
2. `database-path-utils`
|
|
|
|
These aliases avoid importing the shared database barrel from inside the worker,
|
|
which would otherwise pull in runtime code that assumes the main Electron
|
|
process environment.
|
|
|
|
### Worker path resolution
|
|
|
|
`DatabaseWorkerClient` resolves:
|
|
|
|
1. development path from `__dirname`
|
|
2. packaged path from `process.resourcesPath/dist/apps/electron-backend/workers/...`
|
|
3. fallback packaged path from `path.dirname(app.getAppPath())`
|
|
|
|
### Packaged artifact verification
|
|
|
|
`tools/packaging/verify-electron-package-layout.mjs` verifies packaged worker
|
|
artifacts for:
|
|
|
|
1. Linux unpacked resources
|
|
2. macOS app bundles
|
|
3. Windows unpacked app resources
|
|
|
|
The script checks:
|
|
|
|
1. `epg-parser.worker.js`
|
|
2. `database.worker.js`
|
|
3. `better-sqlite3` in one approved unpacked node_modules location
|
|
4. Snap packaging compatibility settings for `better-sqlite3`:
|
|
- `snap.base = core22`
|
|
- Snap launch args keep the X11 fallback
|
|
- Snap and the other non-Flatpak Linux artifacts build on Ubuntu 22.04, while Flatpak builds on a separate Ubuntu 24.04 CI runner
|
|
|
|
### Development rebuild rule
|
|
|
|
Worker-backed database logic is executed from the compiled bundle at:
|
|
|
|
1. `dist/apps/electron-backend/workers/database.worker.js`
|
|
|
|
Do not assume a source edit is active in the live app. If a fix touches:
|
|
|
|
1. `apps/electron-backend/src/app/database/operations/`
|
|
2. `apps/electron-backend/src/app/workers/`
|
|
3. `apps/electron-backend/src/app/events/database/`
|
|
4. preload-backed DB methods consumed by the renderer
|
|
|
|
then the safe workflow is:
|
|
|
|
1. rebuild the worker bundle, or the full `electron-backend` target if preload,
|
|
main-process, or web output also changed
|
|
2. confirm the new `dist/` artifact exists or has a fresh timestamp
|
|
3. restart the Electron process
|
|
4. only then rerun CDP/manual checks or Electron E2E
|
|
|
|
A running Electron app keeps using the worker bundle it already loaded at
|
|
startup. This is a common reason a worker fix appears "not working" in manual
|
|
verification even when the source patch is correct.
|
|
|
|
## Testing
|
|
|
|
### Unit coverage added
|
|
|
|
`apps/electron-backend/src/app/services/database-worker-client.spec.ts`
|
|
covers:
|
|
|
|
1. worker ready -> request -> response flow
|
|
2. event forwarding to a pending request
|
|
3. serialized worker error propagation
|
|
4. `AbortError` propagation for cancelled work
|
|
5. cancel message routing to the live worker
|
|
6. worker exit recovery and fresh worker startup
|
|
|
|
`apps/electron-backend/src/app/events/epg.events.spec.ts` covers:
|
|
|
|
1. shared worker bootstrap usage for the EPG worker
|
|
2. `nativeModuleSearchPaths` forwarding into workerData
|
|
3. actionable worker-path resolution failures
|
|
|
|
`apps/electron-backend/src/app/workers/worker-runtime-paths.spec.ts` covers:
|
|
|
|
1. packaged and development worker path resolution
|
|
2. packaged native-module search path ordering
|
|
3. aggregated native module resolution errors
|
|
|
|
### Electron responsiveness coverage
|
|
|
|
`apps/electron-backend-e2e/src/xtream-responsiveness.e2e.ts` covers:
|
|
|
|
1. large Xtream import shows the overlay promptly
|
|
2. DB worker progress events advance during import
|
|
3. renderer animation frames continue while import/delete are in progress
|
|
4. large Xtream playlist delete shows row-level busy UI and completes cleanly
|
|
|
|
`apps/electron-backend-e2e/src/electron-test-fixtures.ts` now also captures:
|
|
|
|
1. `DB_OPERATION_EVENT` history in the renderer
|
|
2. a requestAnimationFrame counter for repaint assertions
|
|
|
|
For deterministic E2E timing, tests may set:
|
|
|
|
```bash
|
|
IPTVNATOR_DB_WORKER_BATCH_DELAY_MS=20
|
|
```
|
|
|
|
This delay is test-only and disabled by default.
|
|
|
|
### Useful verification commands
|
|
|
|
```bash
|
|
pnpm exec jest --config apps/electron-backend/jest.config.ts --runInBand apps/electron-backend/src/app/services/database-worker-client.spec.ts apps/electron-backend/src/app/events/epg.events.spec.ts apps/electron-backend/src/app/workers/worker-runtime-paths.spec.ts
|
|
pnpm nx run electron-backend:build-worker
|
|
pnpm exec tsc -p apps/electron-backend/tsconfig.app.json --noEmit
|
|
pnpm exec tsc -p apps/web/tsconfig.app.json --noEmit
|
|
pnpm nx run electron-backend-e2e:e2e -- --project=electron --grep "Electron Xtream Responsiveness"
|
|
pnpm run verify:package-layout -- macos arm64
|
|
pnpm run verify:package-layout -- linux
|
|
pnpm run verify:package-layout -- windows
|
|
```
|
|
|
|
### Electron runtime validation
|
|
|
|
```bash
|
|
pnpm nx serve electron-backend
|
|
agent-browser --cdp 9222 tab list
|
|
agent-browser --cdp 9222 tab 1
|
|
agent-browser --cdp 9222 snapshot -i -c -d 3
|
|
pnpm run smoke:packaged -- macos arm64
|
|
```
|
|
|
|
When worker-backed behavior changed, rebuild and restart before reconnecting:
|
|
|
|
```bash
|
|
pnpm nx run electron-backend:build-worker
|
|
CI=1 NX_TASKS_RUNNER_DYNAMIC_OUTPUT=false pnpm nx run electron-backend:build --skip-nx-cache
|
|
stat -f "%Sm %N" dist/apps/electron-backend/workers/database.worker.js
|
|
```
|
|
|
|
Then restart the Electron process and reconnect to `127.0.0.1:9222`.
|
|
|
|
### Electron freeze tracing
|
|
|
|
When a renderer route freezes before DevTools become usable, start Electron with
|
|
one of these opt-in trace flags and inspect the terminal output:
|
|
|
|
```bash
|
|
IPTVNATOR_TRACE_STARTUP=1 pnpm run serve:backend
|
|
```
|
|
|
|
Available trace flags:
|
|
|
|
1. `IPTVNATOR_TRACE_STARTUP=1`
|
|
Enables the broad startup trace set: BrowserWindow lifecycle, renderer
|
|
bridge calls, DB worker requests/events, and SQL tracing.
|
|
2. `IPTVNATOR_TRACE_IPC=1`
|
|
Logs `window.electron.*` method calls crossing the preload bridge so you can
|
|
see whether the renderer is still reaching Electron main.
|
|
3. `IPTVNATOR_TRACE_DB=1`
|
|
Logs `DatabaseWorkerClient` request dispatch, completion timing, and emitted
|
|
`DB_OPERATION_EVENT` payloads.
|
|
4. `IPTVNATOR_TRACE_SQL=1`
|
|
Logs SQLite statements for the shared main-process connection and the DB
|
|
worker connection using `better-sqlite3` verbose hooks.
|
|
5. `IPTVNATOR_TRACE_WINDOW=1`
|
|
Logs BrowserWindow loading, navigation, `unresponsive`, and
|
|
`render-process-gone` transitions.
|
|
6. `IPTVNATOR_TRACE_RENDERER_CONSOLE=1`
|
|
Mirrors renderer console messages into the Electron terminal output when the
|
|
renderer itself is the thing getting wedged.
|
|
|
|
### Electron E2E troubleshooting
|
|
|
|
If a production-mode Electron or Electron E2E launch shows `ERR_FILE_NOT_FOUND`
|
|
for hashed `chunk-*.js`, `main-*.js`, or `styles-*.css` assets:
|
|
|
|
1. treat `dist/apps/web` as stale first
|
|
2. rerun a deterministic production build, for example:
|
|
|
|
```bash
|
|
CI=1 NX_TASKS_RUNNER_DYNAMIC_OUTPUT=false pnpm nx run electron-backend:build --skip-nx-cache
|
|
```
|
|
|
|
3. verify `dist/apps/web/index.html` uses `<base href="./">` before relaunching
|
|
Electron in file-backed mode
|
|
|
|
## Current Limitations
|
|
|
|
These are intentionally still out of scope for this first cut:
|
|
|
|
1. request cancellation
|
|
2. moving network-heavy Xtream fetches off the current path
|
|
3. migrating every remaining small SQLite IPC handler to the worker
|
|
4. richer delete progress reporting for bulk destructive operations
|
|
5. repo-wide Angular/Jest cleanup for the currently failing web test baseline
|
|
|
|
## Extending The Worker
|
|
|
|
When adding another heavy SQLite operation:
|
|
|
|
1. Put SQL-heavy logic in `apps/electron-backend/src/app/database/operations/`.
|
|
2. Add the channel name to `database-worker.types.ts`.
|
|
3. Handle it in `database.worker.ts`.
|
|
4. Proxy the IPC handler through `DatabaseWorkerClient`.
|
|
5. If the renderer needs progress, emit a request-scoped `DbOperationEvent`.
|
|
6. Reuse existing preload/service APIs where possible instead of creating a new
|
|
renderer-facing contract.
|
|
7. Re-run the worker unit test and at least one Electron runtime smoke.
|