29 KiB
SQLite DB Worker
This document records the current non-EPG SQLite worker implementation in the Electron app.
Related:
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_EVENTcontract instead of a single global progress event.
Goals
The worker cutover addresses three concrete problems:
- Main-process UI stalls during large SQLite operations.
- Xtream import progress events were global and unsafe for concurrent jobs.
- EPG and non-EPG writers needed shared SQLite concurrency settings so they
can coexist without
SQLITE_BUSYregressions.
Current Ownership
Main-process runtime wiring
These files own worker lifecycle and IPC bridging:
apps/electron-backend/src/app/services/database-worker-client.tsapps/electron-backend/src/app/events/database/category.events.tsapps/electron-backend/src/app/events/database/content.events.tsapps/electron-backend/src/app/events/database/playlist.events.tsapps/electron-backend/src/app/events/database/xtream.events.tsapps/electron-backend/src/main.ts
Worker runtime
These files own the worker protocol and the SQLite work itself:
apps/electron-backend/src/app/workers/database-worker.types.tsapps/electron-backend/src/app/workers/database.worker.tsapps/electron-backend/src/app/workers/database.worker-connection.tsapps/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:
apps/electron-backend/src/app/database/operations/category.operations.tsapps/electron-backend/src/app/database/operations/content.operations.tsapps/electron-backend/src/app/database/operations/playlist.operations.tsapps/electron-backend/src/app/database/operations/xtream.operations.tsapps/electron-backend/src/app/database/operations/favorites.operations.tsapps/electron-backend/src/app/database/operations/recently-viewed.operations.tsapps/electron-backend/src/app/database/operations/playback-position.operations.tsapps/electron-backend/src/app/database/operations/content-backdrop.operations.tsapps/electron-backend/src/app/database/operations/title-match.operations.tsapps/electron-backend/src/app/database/operations/tmdb.operations.tsapps/electron-backend/src/app/database/operations/epg-mapping.operations.ts
(plus the shared cancellation helper operation-control.ts in the same directory)
Worker Architecture
Request flow
- Renderer calls the existing preload API such as
window.electron.dbSaveContent. ipcMain.handle(...)in the Electron backend builds a payload and delegates toDatabaseWorkerClient.DatabaseWorkerClientlazily starts one long-livedworker_threadsworker and correlates requests with a generatedrequestId.- The worker executes SQLite work and sends back either:
readyeventresponse
- 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:
- worker scripts live under
Resources/dist/apps/electron-backend/workers - unpacked native modules live under one of the approved
app.asar.unpacked/.../node_moduleslocations
Both the EPG worker and the DB worker now share the same runtime helper:
resolveWorkerRuntimeBootstrap(...)for main-process worker launchloadNativeModuleFromSearchPaths(...)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
DbWorkerRequestMessageDbWorkerResponseMessageDbWorkerEventMessageDbOperationEvent
Opt-in request performance capture
IPTVNATOR_PERF_WORKER_PROFILING=1 adds development/test-only performance
metadata to each database-worker response. It is disabled by default and must
stay disabled for production launches.
Each enabled request gets a fresh event-loop-delay histogram and records:
requestReceivedEpochMs,workStartedEpochMs,workEndedEpochMs, andhistogramFlushedEpochMs- worker-thread CPU user/system microseconds from
process.threadCpuUsage() - event-loop utilization across the exact work interval
- event-loop-delay max/p95/p99 from the request's own histogram
- fixed invalid or unavailable reasons whenever a metric cannot be attributed
Histogram arming waits until the histogram has a sample; flushing waits for its
sample count to advance after work ends. Both waits use condition-based timer
polling. Each wait stops after 50 ms of observed monotonic time or its bounded
poll count; arming and flushing have separate caps, and timer scheduling may
overshoot wall-clock time. A timeout or profiling API failure never replaces
the business response: timestamps and any independently available CPU/ELU
metrics remain valid, while event-loop delay is null with a fixed reason.
The long-lived database worker still executes concurrent requests without a
profiling queue. If captures overlap, every overlapping response carries
invalidReason: "overlapping-database-worker-requests" and all attributable
CPU, ELU, and event-loop-delay values are null. This avoids assigning shared
worker activity to one request while preserving normal worker concurrency.
Progress event contract
The worker now emits request-scoped events with:
operationIdoperationplaylistIdstatus- optional
phase - optional
current - optional
total - optional
increment
Current shipped operation names:
save-contentdelete-xtream-contentrestore-xtream-user-datadelete-playlistdelete-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:
DB_CANCEL_OPERATIONwindow.electron.dbCancelOperation(operationId)DatabaseService.cancelOperation(operationId)
If a worker operation is canceled:
- the worker emits a final
cancelledevent - the request rejects with an
AbortError - 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
onDbOperationEvent(callback)dbSaveContent(playlistId, streams, type, operationId?)dbDeleteXtreamContent(playlistId, operationId?)dbRestoreXtreamUserData(..., operationId?)dbDeletePlaylist(playlistId, operationId?)dbDeleteAllPlaylists(operationId?)dbCancelOperation(operationId)- legacy compatibility:
onDbSaveContentProgress(callback)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:
createOperationId(...)cancelOperation(operationId)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
DB_HAS_CATEGORIESDB_GET_CATEGORIESDB_SAVE_CATEGORIESDB_GET_ALL_CATEGORIESDB_UPDATE_CATEGORY_VISIBILITY
Content
DB_HAS_CONTENTDB_GET_CONTENTDB_SAVE_CONTENTDB_GET_CONTENT_BY_XTREAM_IDDB_SEARCH_CONTENTDB_GLOBAL_SEARCHDB_GET_GLOBAL_RECENTLY_ADDED
DB_GLOBAL_SEARCH returns a shared global-search result union rather than an
Xtream-only row shape:
source_type = "xtream"rows come from the normalizedcontentandcategoriestables joined withplaylists. Xtream title matching usescontent_title_fts, an FTS5 trigram index overcontent.title, for search terms with at least one 3+ character token. Tokens are quoted before being passed toMATCH, so FTS reserved words such asandare treated as search text instead of degrading to the slow fallback path. SQL prefilters keep both raw lower-case tokens and accent-normalized tokens, so an accented query such asCafécan still reach rows stored as eitherCaféorCafebefore the worker's accent-insensitive ranking step. Existing databases rebuild this index once through themigration:content-title-fts-trigram:v1app-state marker before legacy content cleanup/normalization migrations run; fresh inserts, deletes, and title updates stay synchronized through SQLite triggers.source_type = "m3u"rows come from M3U playlist payloads stored inplaylists.payload. The worker uses SQLpayload LIKEagainst channelname/titleJSON fields only as a coarse candidate prefilter, then parses candidate JSON payloads and matches channel name, TVG name, and group title case-insensitively in the worker. M3U payload prefilters also preserve raw accented token variants next to normalized variants. The SQL candidate query is capped by the same stable 5000-row candidate limit used for Xtream search, so large matching M3U payloads are not loaded without an upper bound.- The optional
sourcesargument can restrict search toxtreamorm3u, but omitted callers keep the backward-compatible behavior of searching all supported global-search sources. - The optional pagination argument accepts
{ limit, offset }. The worker keeps the legacy default of 50 results when the argument is omitted, but the routed global-search view requests one extra row per page to implement lazy loading without requiring a separate count query. Candidate selection uses a stable max-size pool for every page so score-based in-memory ranking cannot shift already-rendered items into later pages. - Candidate rows are ranked in the worker after the coarse SQL prefilter.
Exact and prefix matches sort ahead of word-prefix and substring matches;
short first tokens such as
tvstay anchored to the start of the title, soTV Sportmatches butTest TVdoes not. These short first-token queries bypass trigram FTS and use theidx_content_titleprefix index path because trigram tokenization cannot match 1-2 character terms. Punctuation-joined words such asA&EorX-Menare an exception: tokenization splits them into short fragments, so the intact word is preserved as a "compound word" (content-search.util.ts) that additionally matches as an exact substring — a supplemental trigram FTSMATCH '"a&e"'query for the Xtream arm (merged and deduped with the prefix-index candidates), intact-wordLIKEcontains patterns for the per-playlist and M3U payload prefilters, and a space-bounded whole-phrase check in the ranking step. All compound arms keep the remaining words of the query as SQL constraints — the FTS supplement AND-s the non-compound tokens asLIKEconditions and theLIKEprefilters compose per word — soA&E HDcannot fill the bounded candidate window with titles that only containA&E. This letsA&EfindUS: A&Eanywhere in the title while single short tokens stay prefix-anchored (issue #1161). excludeHiddenstill filters hidden Xtream categories and also filters M3U channels whosegroup.titleis listed in the playlist payload'shiddenGroupTitles.- M3U radio entries are returned as live results with
radio = "true"and the serializedChannelattached for renderer playback routing. M3U global search rows are not Xtream content rows: they usextream_id = -1, and Xtream-only metadata such asrating,added, and a missingposter_urlis represented asnull. - EPG-program text is not part of this contract; EPG search remains a separate feature because it uses different persistence and freshness rules.
Playlist metadata
DB_CREATE_PLAYLISTDB_UPSERT_APP_PLAYLISTDB_UPSERT_APP_PLAYLISTSDB_GET_APP_PLAYLISTSDB_GET_APP_PLAYLIST_METASDB_GET_APP_PLAYLISTDB_GET_APP_PLAYLIST_FAVORITE_CHANNELSDB_GET_PLAYLISTDB_UPDATE_PLAYLISTDB_DELETE_PLAYLISTDB_DELETE_ALL_PLAYLISTSDB_GET_APP_STATEDB_SET_APP_STATE
DB_GET_APP_PLAYLIST_METAS is the preferred path for summary surfaces such as
the workspace sidebar and dashboard source rail. It selects playlist metadata
columns only and deliberately skips the large payload column, which can
contain full parsed M3U channel lists. Full playlist reads must continue using
DB_GET_APP_PLAYLIST for one playlist or DB_GET_APP_PLAYLISTS for legacy
full-data workflows.
DB_GET_APP_PLAYLIST_FAVORITE_CHANNELS is a dashboard-oriented M3U fast path.
It resolves a playlist's favorite IDs to matching channel payloads inside the
DB worker and returns only the matched channels plus favorite order metadata.
Renderer code must still fall back to DB_GET_APP_PLAYLIST when the fast path
is unavailable or the SQLite playlist migration has not completed.
Xtream refresh helpers
DB_DELETE_XTREAM_CONTENTDB_RESTORE_XTREAM_USER_DATA
Favorites
DB_ADD_FAVORITEDB_REMOVE_FAVORITEDB_IS_FAVORITEDB_GET_FAVORITESDB_GET_GLOBAL_FAVORITESDB_GET_ALL_GLOBAL_FAVORITESDB_REORDER_GLOBAL_FAVORITES
Recently viewed
DB_GET_RECENTLY_VIEWEDDB_CLEAR_RECENTLY_VIEWEDDB_GET_RECENT_ITEMSDB_ADD_RECENT_ITEMDB_CLEAR_PLAYLIST_RECENT_ITEMSDB_REMOVE_RECENT_ITEM
Playback positions
DB_SAVE_PLAYBACK_POSITIONDB_GET_PLAYBACK_POSITIONDB_GET_SERIES_PLAYBACK_POSITIONSDB_GET_RECENT_PLAYBACK_POSITIONSDB_GET_ALL_PLAYBACK_POSITIONSDB_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:
foreign_keys = ONjournal_mode = WALbusy_timeout = 5000
Current sources:
libs/shared/database/src/lib/connection.tsapps/electron-backend/src/app/workers/database.worker-connection.tsapps/electron-backend/src/app/workers/epg-parser.worker.ts
Main-process EPG ownership is split across focused event modules:
apps/electron-backend/src/app/events/epg.events.tsregisters EPG IPC handlers and owns freshness/fetch orchestration.apps/electron-backend/src/app/events/epg-worker.service.tsowns EPG worker creation, renderer progress updates, fetch worker lifecycle, and clear-worker lifecycle.apps/electron-backend/src/app/events/epg-query.service.tsowns EPG channel/program database lookups, metadata resolution, and DB row mapping.
Keep worker lifecycle state out of the IPC registration layer. Add new EPG DB
lookup behavior to epg-query.service.ts; add new EPG worker/progress behavior
to epg-worker.service.ts.
EPG fetch workers use an inactivity watchdog, not a fixed maximum import
duration. EpgWorkerService starts the watchdog when the worker is created,
refreshes it when the worker becomes ready, and refreshes it again whenever an
EPG_PROGRESS event increases the channel or program counters. This lets very
large XMLTV imports continue for longer than the nominal timeout as long as the
parser/database pipeline is still making progress, while still terminating a
worker that stops emitting progress.
UI Behavior Changes
Search
Xtream search now guards against stale async responses:
- local playlist search uses a monotonically increasing request version
- global search uses a separate request version in the routed workspace search component
- 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:
xtream_idcan collide acrosslive,movie, andserieswithin the same playlist.- 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_idxtream_idcontent.type
- Mixed Xtream collection identity must key entries by
type + xtream_id, notxtream_idalone. 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_idcan favorite the wrong persisted row when IDs collide. - Continue-watching / recently-viewed flows that resolve by only
playlist_id + xtream_idcan store the wrong persisted row when a series or movie ID collides with a live entry. - Mixed favorites maps keyed only by
xtream_idcan 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_idcan cause local UI state to remove, reorder, or reactivate the wrong Xtream entry when different content types collide.
Current implementation paths:
apps/electron-backend/src/app/database/operations/content.operations.tslibs/portal/xtream/data-access/src/lib/with-favorites.feature.tslibs/portal/xtream/data-access/src/lib/with-recent-items.tslibs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.tslibs/portal/shared/util/src/lib/collection/unified-recent-data.service.tslibs/portal/shared/util/src/lib/collection/unified-favorites-data.service.ts
Busy states
The UI now has explicit long-running state for destructive operations:
- recent playlist rows show row-level refresh/delete spinners
- Xtream import overlay shows phase text and a cancel action
- Xtream playlist rows show request-scoped progress and cancel actions
- busy rows block repeat clicks while an operation is in flight
- 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:
epg-parser.worker.tsdatabase.worker.ts
The worker build also aliases:
@iptvnator/shared/database/schema@iptvnator/shared/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:
- development path from
__dirname - packaged path from
process.resourcesPath/dist/apps/electron-backend/workers/... - fallback packaged path from
path.dirname(app.getAppPath())
Packaged artifact verification
tools/packaging/verify-electron-package-layout.mjs verifies packaged worker
artifacts for:
- Linux unpacked resources
- macOS app bundles
- Windows unpacked app resources
The script checks:
epg-parser.worker.jsdatabase.worker.jsbetter-sqlite3in one approved unpacked node_modules location- 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:
dist/apps/electron-backend/workers/database.worker.js
Do not assume a source edit is active in the live app. If a fix touches:
apps/electron-backend/src/app/database/operations/apps/electron-backend/src/app/workers/apps/electron-backend/src/app/events/database/- preload-backed DB methods consumed by the renderer
then the safe workflow is:
- rebuild the worker bundle, or the full
electron-backendtarget if preload, main-process, or web output also changed - confirm the new
dist/artifact exists or has a fresh timestamp - restart the Electron process
- 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.
Gotchas
Prepared-statement writes inside a transaction must use .run(), not .execute()
Drizzle's PreparedQuery.execute() on the better-sqlite3 driver returns a
promise and defers the actual SQL to a microtask. Our bulk writers run their
statements inside a synchronous db.transaction(() => { ... }) callback,
which cannot await. If the statement is dispatched with .execute(), the
transaction commits before the deferred promise settles, so the write is a
silent no-op — no error, no rows changed.
Always call the synchronous .run(placeholderValues) on prepared statements
executed inside a synchronous transaction callback:
// favorites is playlist-scoped: filter by (contentId, playlistId), otherwise
// a same-contentId favorite in another playlist gets rewritten too.
const stmt = db.update(schema.favorites)
.set({ position: sql<number>`${sql.placeholder('position')}` })
.where(
and(
eq(schema.favorites.contentId, sql.placeholder('contentId')),
eq(schema.favorites.playlistId, sql.placeholder('playlistId'))
)
)
.prepare();
db.transaction(() => {
for (const { content_id, playlist_id, position } of chunk) {
// NOT .execute()
stmt.run({ position, contentId: content_id, playlistId: playlist_id });
}
});
This bit reorderGlobalFavorites and removeRecentItemsBatch (issue #1137):
custom favorites drag-and-drop order silently never persisted for the
per-playlist ("this playlist") view. Global ("all playlists") favorites masked
it because that path also persists an order to the appState
global-favorites-channel-order-v1 key and re-applies it on read, independent
of the DB position column. The mocked operations specs did not catch it —
a jest mock records an .execute() call the same as a .run() call, so the
regression tests explicitly assert .run() is used and .execute() is not.
Testing
Unit coverage added
apps/electron-backend/src/app/services/database-worker-client.spec.ts
covers:
- worker ready -> request -> response flow
- event forwarding to a pending request
- serialized worker error propagation
AbortErrorpropagation for cancelled work- cancel message routing to the live worker
- worker exit recovery and fresh worker startup
apps/electron-backend/src/app/events/epg.events.spec.ts covers:
- shared worker bootstrap usage for the EPG worker
nativeModuleSearchPathsforwarding into workerData- actionable worker-path resolution failures
- EPG clear worker rejection on unexpected exit or timeout
- case-insensitive EPG program lookup fallbacks
- metadata lookup precedence for exact/case-insensitive id and display name
- malformed EPG row filtering
- active EPG fetches keep running when worker progress keeps moving
apps/electron-backend/src/app/workers/worker-runtime-paths.spec.ts covers:
- packaged and development worker path resolution
- packaged native-module search path ordering
- aggregated native module resolution errors
Electron responsiveness coverage
apps/electron-backend-e2e/src/xtream-responsiveness.e2e.ts covers:
- large Xtream import shows the overlay promptly
- DB worker progress events advance during import
- renderer animation frames continue while import/delete are in progress
- large Xtream playlist delete shows row-level busy UI and completes cleanly
apps/electron-backend-e2e/src/electron-test-fixtures.ts now also captures:
DB_OPERATION_EVENThistory in the renderer- a requestAnimationFrame counter for repaint assertions
For deterministic E2E timing, tests may set:
IPTVNATOR_DB_WORKER_BATCH_DELAY_MS=20
This delay is test-only and disabled by default.
Useful verification commands
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
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:
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:
IPTVNATOR_TRACE_STARTUP=1 pnpm run serve:backend
Available trace flags:
IPTVNATOR_TRACE_STARTUP=1Enables the broad startup trace set: BrowserWindow lifecycle, renderer bridge calls, DB worker requests/events, and SQL tracing.IPTVNATOR_TRACE_IPC=1Logswindow.electron.*method calls crossing the preload bridge so you can see whether the renderer is still reaching Electron main.IPTVNATOR_TRACE_DB=1LogsDatabaseWorkerClientrequest dispatch, completion timing, and emittedDB_OPERATION_EVENTpayloads.IPTVNATOR_TRACE_SQL=1Logs SQLite statements for the shared main-process connection and the DB worker connection usingbetter-sqlite3verbose hooks.IPTVNATOR_TRACE_WINDOW=1Logs BrowserWindow loading, navigation,unresponsive, andrender-process-gonetransitions.IPTVNATOR_TRACE_RENDERER_CONSOLE=1Mirrors 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:
- treat
dist/apps/webas stale first - rerun a deterministic production build, for example:
CI=1 NX_TASKS_RUNNER_DYNAMIC_OUTPUT=false pnpm nx run electron-backend:build --skip-nx-cache
- verify
dist/apps/web/index.htmluses<base href="./">before relaunching Electron in file-backed mode
Current Limitations
These are intentionally still out of scope for this first cut:
- moving network-heavy Xtream fetches off the current path
- migrating every remaining small SQLite IPC handler to the worker
- richer delete progress reporting for bulk destructive operations
- repo-wide Angular/Jest cleanup for the currently failing web test baseline
(Request cancellation, originally listed here, has since shipped — see the
"Cancellation contract" section above: DB_CANCEL_OPERATION in
apps/electron-backend/src/app/api/main.preload.ts, AbortError production in
database.worker.ts, and DatabaseService.cancelOperation in
libs/services/src/lib/database-electron.service.ts.)
Extending The Worker
When adding another heavy SQLite operation:
- Put SQL-heavy logic in
apps/electron-backend/src/app/database/operations/. - Add the channel name to
database-worker.types.ts. - Handle it in
database.worker.ts. - Proxy the IPC handler through
DatabaseWorkerClient. - If the renderer needs progress, emit a request-scoped
DbOperationEvent. - Reuse existing preload/service APIs where possible instead of creating a new renderer-facing contract.
- Re-run the worker unit test and at least one Electron runtime smoke.