
IPTVnator SQLite DB Worker 架构解析Electron 主进程解耦、请求级进度与协作式取消【免费下载链接】iptvnator:tv: Cross-platform IPTV player application with multiple features, such as support of m3u and m3u8 playlists, favorites, TV guide, TV archive/catchup and more.项目地址: https://gitcode.com/GitHub_Trending/ip/iptvnator导读IPTVnator 的 Electron 桌面端曾面临一个经典问题大型 Xtream 与播放列表的 SQLite 写入操作阻塞主进程导致界面冻结。本篇文章基于仓库中的开发指南与源码实现系统讲解 IPTVnator 如何通过一个长驻的worker_threads数据库工作线程DB Worker承接全部重型非 EPG SQLite 工作用requestId/operationId双身份模型实现请求级进度事件与协作式取消并给出目录批量写入、打包路径解析、开发期重建与验证的完整实战方案。读完本文你将掌握该项目的数据库分层职责、消息协议、取消语义、SQLite 并发规则以及新增重型数据库操作的标准流程。1. 为什么需要独立的数据库工作线程1.1 三个必须解决的问题根据 docs/architecture/sqlite-db-worker.md 的记录这次工作线程化改造针对三个具体痛点主进程 UI 卡顿大型 SQLite 操作在 Electron 主线程上执行会阻塞窗口绘制。全局进度事件不安全Xtream 导入进度此前通过单一全局进度事件广播并发任务互相污染。EPG 与非 EPG 写入需要共享并发设置两类写入方必须使用兼容的 SQLite pragma否则会出现SQLITE_BUSY回归。1.2 改造后的职责边界改造后的边界非常明确重型非 EPG SQLite 工作不再运行在 Electron 主线程由一条长驻数据库工作线程承接慢速的 Xtream 与播放列表数据库操作刻意轻量的下载与 EPG 专属处理器仍留在主进程详见下文“刻意保留在主进程的例外”渲染进程 API 保持稳定主要变化是进度与长任务状态改由请求级request-scoped的DB_OPERATION_EVENT契约承载取代旧的单一全局进度事件。2. 代码所有权与完整调用链2.1 一条端到端的调用链从 .codex/skills/iptvnator-sqlite-db-worker/SKILL.md 可以看到这条链路渲染进程 DatabaseService → preload IPC 桥 → 主进程事件处理器 → DatabaseWorkerClient 与请求协议 → 工作线程 dispatcher → 工作线程连接SQLite 连接 → 操作模块 → 共享 schemaiptvnator/shared/database/schema → 响应或请求级事件回到发起请求的渲染进程这条链上每个环节都有明确的归属文件各司其职层次文件职责渲染进程服务libs/services/src/lib/database-electron.service.ts渲染进程数据库服务生成operationId、订阅事件preload 桥apps/electron-backend/src/app/api/main.preload.ts暴露window.electron.db*稳定 APIIPC 处理器apps/electron-backend/src/app/events/database/按分类category/content/playlist/xtream注册ipcMain.handle主进程客户端apps/electron-backend/src/app/services/database-worker-client.ts懒启动长驻 worker、按requestId关联请求协议类型apps/electron-backend/src/app/workers/database-worker.types.ts消息契约与操作名/阶段常量薄分发器apps/electron-backend/src/app/workers/database.worker.ts专注 dispatch 与编排不写 SQL连接管理apps/electron-backend/src/app/workers/database.worker-connection.tsworker 内 SQLite 连接与 pragma运行时路径apps/electron-backend/src/app/workers/worker-runtime-paths.ts开发/打包两种 worker 与原生模块路径解析SQL 操作apps/electron-backend/src/app/database/operations/全部 SQL 重活共享 schemalibs/shared/database/src/lib/schema.ts表契约通过iptvnator/shared/database/schema引入打包器apps/electron-backend/build-worker.js产出 worker 产物2.2 操作模块的职责划分SQL 重逻辑全部收敛在 apps/electron-backend/src/app/database/operations/ 目录包括category.operations.ts、content.operations.ts、playlist.operations.ts、xtream.operations.ts、favorites.operations.ts、recently-viewed.operations.ts、playback-position.operations.ts、tmdb.operations.ts、title-sources.operations.ts、vod-source-pin.operations.ts等十余个模块并配有content-search.util.ts、title-token-glob.ts、operation-control.ts、performance-phase-capture.ts、xtream-content-operation-steps.ts等辅助文件。这样划分的核心原则是让 worker 入口保持为“薄分发器”——从 database.worker.ts 的executeRequest可以看到它只做 payload 类型收窄、调用对应 operation 模块、编排started/progress/completed事件SQL 一律在 operation 模块内部完成。3. 请求级协议requestId 与 operationId 的双身份模型3.1 消息类型工作线程协议定义在 database-worker.types.ts核心消息有四类DbWorkerRequestMessage渲染进程/主进程 → workerDbWorkerResponseMessageworker → 主进程DbWorkerEventMessageworker → 主进程的请求级事件DbOperationEvent请求级操作事件负载worker 只会向主进程回送三种消息ready就绪、event请求级事件、response响应。3.2 两种身份的不同作用域协议里最容易混淆的是requestId与operationId二者作用域完全不同requestId由DatabaseWorkerClient在每次请求时用randomUUID()生成用于把 worker 返回的event/response与主进程侧挂起的传输请求关联起来。它是传输层身份渲染进程不可见也不代表业务状态。operationId由渲染进程为被跟踪的长任务提供是渲染进程可见的业务身份贯穿 renderer → preload → main → worker 全程进度事件与协作式取消都依赖它。在 database-worker-client.ts 的request()实现中可以看到每个请求都先await this.ensureWorker()懒启动再生成requestId并把{ resolve, reject, onEvent, operation }存入pendingRequests映射最后postMessage({ type: request, requestId, operation, payload })。3.3 进度事件契约DbOperationEvent的字段如下operationId可选operation操作名playlistId可选statusstarted | progress | completed | cancelled | errorphase可选阶段名current/total可选进度数值increment可选增量3.4 被跟踪的操作与可取消性当前有五个被跟踪tracked的操作名称常量定义在 database-worker.types.ts 的DB_OPERATION_NAMES操作名说明可取消save-content保存内容✅delete-xtream-content删除 Xtream 内容✅restore-xtream-user-data恢复 Xtream 用户数据✅delete-playlist删除播放列表✅delete-all-playlists删除全部播放列表❌刻意不可取消注意delete-all-playlists是刻意以cancellable: false跟踪的其渲染进程可见进度会被跟踪但取消请求不会中断它。这一点在 database.worker.ts 的DB_DELETE_ALL_PLAYLISTS分支中直接体现executeTrackedOperation传入cancellable: false。3.5 协作式取消的语义取消是**协作式cooperative**的落在批提交的检查点checkpoint之间渲染进程通过DB_CANCEL_OPERATION/window.electron.dbCancelOperation(operationId)/DatabaseService.cancelOperation(operationId)请求取消worker 收到cancel消息后把activeOperations中对应operationId的状态置为cancelled true操作模块在下一个检查点发现该标记抛出AbortErrorworker 发出最终的cancelled事件主进程将该请求以AbortError拒绝rejectUI 清除忙碌状态但不把操作当作成功。关键的语义约定已提交的 SQLite 批次保持已提交不会回滚对于忙碌生命周期只有终态事件completed/error/cancelled才能最终确定 UI 状态UI 可以立即设置一个独立的“已请求取消”标记防止在等待权威终态事件期间重复点击取消按钮。在 database.worker.ts 的checkpoint()中可以看到检查点的双重校验先检查取消标记然后await pauseBetweenBatches()默认setImmediate让出事件循环一拍让排队中的取消消息有机会到达再检查一次取消标记。这种“让出一拍”的设计保证了取消消息能在下一批开始前被 worker 收到。4. 为什么用一条长驻工作线程选用单条长驻 worker 而非“按需启停”或“每请求一线程”理由有三避免每次搜索/删除/导入都付出 worker 启动成本集中化故障处理与重启行为DatabaseWorkerClient在 workererror/exit时统一拒绝所有挂起请求并重置状态下一次请求会重新创建 worker见 database-worker-client.ts 的handleWorkerFailure/handleWorkerExit/resetWorkerState与既有 EPG worker 保持一致不增殖可写 SQLite 的所有者owner。4.1 打包环境下的 worker 引导打包后的 Electron 应用worker 脚本与原生模块如better-sqlite3所在位置并不相同worker 脚本位于Resources/dist/apps/electron-backend/workers未打包进 asar 的原生模块位于app.asar.unpacked/.../node_modules下的指定位置。EPG worker 与 DB worker 现在共享同一套运行时助手worker-runtime-paths.tsresolveWorkerRuntimeBootstrap(...)主进程侧解析 worker 启动信息loadNativeModuleFromSearchPaths(...)worker 侧按搜索路径加载原生模块。路径解析以process.resourcesPath为主path.dirname(app.getAppPath())仅作回退。原生模块候选路径按固定顺序尝试app.asar.unpacked/node_modules app.asar.unpacked/electron-backend/node_modules app.asar.unpacked/dist/apps/electron-backend/node_modules解析失败会抛出带完整候选清单的WorkerPathResolutionError/NativeModuleResolutionError便于快速定位打包缺失。5. 目录批量写入行预算事务与 FTS5 同步5.1 为什么不能每 100 行提交一次仓库在“Catalog write batching”一节记录了这条经验的来历此前目录删除/插入按每 100 行提交一次一个 30 万行的播放列表会付出约 3,000 次提交和约 2 GB 的 WAL 流量单事务写入则只有 140 MB。但巨型单事务也不是答案——因为worker 只在 await 之间服务其他请求取消只能在提交之间协作式落点主进程与 EPG worker 的连接在写事务持有锁期间会因 5 秒busy_timeout而放弃。因此最终方案是行预算row-budgeted事务。5.2 关键参数与实现策略CONTENT_ROWS_PER_TRANSACTION 5,000删除与插入两个方向的预算均为 5,000 行定义于 apps/electron-backend/src/app/database/operations/catalog-deletion.ts。删除不在 JavaScript 中物化行 id。worker 通过两个覆盖索引按分类读取内容行数countContentRowsByCategory把连续分类按预算分组成组groupCategoriesByRowBudget超过预算的大分类自成一组整组删除实际最大约 4.5 万行的分类仍能在远低于 1 秒内提交每组只发一条DELETE FROM content WHERE category_id IN (...)分类随后用收集步骤捕获的 id 一条语句删除——绝不用整个播放列表范围的谓词因为同一播放列表可能有更新的导入创建了新分类旧刷新不能误删。播放列表删除是唯一按 playlist id 圈定作用域的地方。插入每条 INSERT 保持 100 行Drizzle 绑定XtreamContentValue提供的 11 列即每条语句 1,100 个参数整批 5,000 行单语句会超过 SQLite 的 32,766 参数上限每 50 条 INSERT 包进一个事务。进度语义删除时current是 SQLite 报告的changes之和、total是预先求和的计数插入时current是交给INSERT ... ON CONFLICT DO NOTHING的行数、total是归一化后的值——因此被冲突跳过的重复行仍会计入进度这是进度数字不是行数。每次提交前都执行检查点保证取消能恰好落在提交之间。deleteCategoriesWhere的过滤条件会经过requireScopedFilter拒绝空and()产生的undefined——因为.where(undefined)会变成全表删除。5.3 进度节流进度事件在 worker 内节流apps/electron-backend/src/app/workers/operation-progress-throttle.ts同一操作每 100 ms 至多一条progress事件其余合并进下一条保留最新的phase/current/totalincrement求和保证按增量累加的消费方最终得到相同总数。阶段的首条上报、达到total的上报、离开阶段时挂起的上报永远不会被扣留终态completed/cancelled/error之前会先 flush 挂起事件。因此消费方不能假设“每提交一个事务就有一条事件”。6. SQLite 并发规则EPG 与 DB worker 共存EPG 解析仍在其专属 worker 中两条 worker 必须使用兼容的 pragma。当前在主进程共享连接路径与 worker 自有连接中统一应用foreign_keys ONjournal_mode WALbusy_timeout 5000来源文件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主进程侧的 EPG 所有权按职责拆分在多个聚焦模块中epg.events.ts只负责注册 IPC 并委托epg-fetch.service.ts负责新鲜度检查与多 URL 拉取编排epg-mapping.service.ts负责手动频道映射的 CRUDepg-worker.service.ts负责 worker 生命周期与进度epg-query.service.ts负责频道/节目查询与行映射。worker 生命周期状态不得放进 IPC 注册层。另外EPG 拉取 worker 使用不活动看门狗而非固定最大导入时长EpgWorkerService在 worker 创建时启动看门狗就绪时刷新每当EPG_PROGRESS事件推进频道或节目计数时再次刷新。这允许超大型 XMLTV 导入在管线持续产出进度时超过名义超时同时仍能终止停止产出进度的 worker。7. 已迁移的操作与刻意保留的例外7.1 worker 拥有的操作分组从 database-worker.types.ts 的DB_WORKER_OPERATIONS常量看worker 目前承载了约 70 个操作按业务分组如下分类DB_HAS_CATEGORIES、DB_GET_CATEGORIES、DB_SAVE_CATEGORIES、DB_GET_ALL_CATEGORIES、DB_UPDATE_CATEGORY_VISIBILITY内容DB_HAS_CONTENT、DB_GET_CONTENT、DB_SAVE_CONTENT、DB_CLEAR_XTREAM_IMPORT_CACHE、DB_GET_CONTENT_BY_XTREAM_ID、DB_SEARCH_CONTENT、DB_GLOBAL_SEARCH、DB_GET_GLOBAL_RECENTLY_ADDED播放列表元数据DB_CREATE_PLAYLIST、DB_UPSERT_APP_PLAYLIST、DB_UPSERT_APP_PLAYLISTS、DB_GET_APP_PLAYLISTS、DB_GET_APP_PLAYLIST_METAS、DB_GET_APP_PLAYLIST、DB_GET_APP_PLAYLIST_FAVORITE_CHANNELS、DB_GET_PLAYLIST、DB_UPDATE_PLAYLIST、DB_DELETE_PLAYLIST、DB_DELETE_ALL_PLAYLISTS、DB_GET_APP_STATE、DB_SET_APP_STATEXtream 刷新辅助DB_DELETE_XTREAM_CONTENT、DB_RESTORE_XTREAM_USER_DATA收藏DB_ADD_FAVORITE、DB_REMOVE_FAVORITE、DB_IS_FAVORITE、DB_GET_FAVORITES、DB_GET_GLOBAL_FAVORITES、DB_GET_ALL_GLOBAL_FAVORITES、DB_REORDER_GLOBAL_FAVORITES最近观看DB_GET_RECENTLY_VIEWED、DB_CLEAR_RECENTLY_VIEWED、DB_GET_RECENT_ITEMS、DB_ADD_RECENT_ITEM、DB_CLEAR_PLAYLIST_RECENT_ITEMS、DB_REMOVE_RECENT_ITEM、DB_REMOVE_RECENT_ITEMS_BATCH播放位置DB_SAVE_PLAYBACK_POSITION、DB_GET_PLAYBACK_POSITION、DB_GET_SERIES_PLAYBACK_POSITIONS、DB_GET_RECENT_PLAYBACK_POSITIONS、DB_GET_ALL_PLAYBACK_POSITIONS、批量保存/清除等此外还有 TMDB 元数据缓存、标题匹配、标题来源、VOD 源钉选source pin等操作。7.2 刻意保留在主进程的例外以下路径不要作为无关数据库改造的一部分随意迁移除非它们变重到阻塞主进程才需重新评估边界apps/electron-backend/src/app/events/database/downloads.events.ts小型下载行读写紧邻原生对话框、文件系统清理与主进程下载运行时apps/electron-backend/src/app/events/database/epg-db.events.tsEPG 节目搜索 IPC 处理器epg-fetch.service.ts、epg-mapping.service.ts、epg-query.service.tsEPG 新鲜度、映射与查询保持在聚焦的主进程所有者中EPG 解析/导入仍在其专属 worker。7.3 两个值得注意的查询优化DB_GET_APP_PLAYLIST_METAS是汇总面侧边栏、dashboard 源栏的首选路径只选播放列表元数据列刻意跳过可能包含完整 M3U 频道列表的大payload列完整读取仍走DB_GET_APP_PLAYLIST单列表或DB_GET_APP_PLAYLISTS旧的全量工作流。DB_GLOBAL_SEARCH返回共享的全局搜索结果联合而非 Xtream 专属行结构Xtream 来源命中contentcategories关联playlists标题匹配使用 FTS5 trigram 索引content_title_fts搜索词含至少一个 3 字符 token 时启用token 先加引号再进MATCH避免and等 FTS 保留字退化到慢回退路径同时保留原生小写与重音归一化 token 两个变体保证Café这类重音查询能命中Café或Cafe存储的行M3U 来源则对playlists.payload做 SQLpayload LIKE粗筛后再在 worker 内解析 JSON 做大小写不敏感匹配候选上限与 Xtream 搜索一致为 5,000 行。8. 渲染进程契约与 UI 行为变化8.1 preload 桥 APIpreload 桥在保留既有数据库方法的同时新增请求级事件通道。重要 API见 main.preload.tsonDbOperationEvent(callback)dbSaveContent(playlistId, streams, type, operationId?)dbDeleteXtreamContent(playlistId, operationId?)dbRestoreXtreamUserData(..., operationId?)dbDeletePlaylist(playlistId, operationId?)dbDeleteAllPlaylists(operationId?)dbCancelOperation(operationId)旧版兼容onDbSaveContentProgress(callback)/removeDbSaveContentProgress()DatabaseService.saveXtreamContent(...)现在自行生成operationId、订阅onDbOperationEvent、按该operationId过滤事件只有在新事件通道缺失时才回退到旧版进度 API。DatabaseService还持有createOperationId(...)、cancelOperation(operationId)、isDbAbortError(error)。8.2 类型感知的内容查找xtream_id 只是部分唯一一个重要的数据契约xtream_id在同一播放列表内的live、movie、series之间可能冲突。任何从 Xtream 结果卡片、收藏按钮、最近条目更新、继续观看流程或详情路由发起的数据库查找都必须以playlist_idxtream_idcontent.type三元组解析混合 Xtream 集合收藏映射、最近条目列表、dashboard 集合负载必须用type xtream_id作为键而不是单独的xtream_id。否则仅按playlist_id xtream_id解析收藏会收藏到错误的持久化行继续观看/最近观看流程在剧集或电影 id 与直播条目冲突时会存错行仅按xtream_id键控的混合收藏映射会把无关的直播行标记为已收藏仅按xtream_id键控的混合最近/收藏条目会导致本地 UI 状态误删、重排或重激活错误的 Xtream 条目。相关实现路径content.operations.ts、libs/portal/xtream/data-access/src/lib/with-favorites.feature.ts、libs/portal/xtream/data-access/src/lib/with-recent-items.ts、libs/portal/shared/data-access/src/lib/collection/unified-recent-data.service.ts 等。8.3 忙碌状态UI 现在为破坏性操作提供显式的长任务状态最近播放列表行显示行级刷新/删除 spinnerXtream 导入浮层显示阶段文本与取消动作Xtream 播放列表行显示请求级进度与取消动作忙碌行在操作进行中阻止重复点击设置页“移除所有播放列表”拥有自己的 spinner/禁用态并消费请求级 DB 操作事件渲染进度文本。这些变化之所以重要是因为 SQLite 工作离开主线程后渲染进程才能真正绘制出加载状态而不是冻结。8.4 搜索的陈旧响应防护Xtream 搜索现在防住了陈旧异步响应本地播放列表搜索使用单调递增的请求版本号全局搜索在 routed workspace 搜索组件中维护独立版本号清空搜索会使更早的挂起结果失效——防止旧的 worker 响应覆盖新查询或已清空的搜索状态。9. 一个必须记住的坑同步事务内.run()而不是.execute()Drizzle 在better-sqlite3驱动上的PreparedQuery.execute()返回Promise把真正的 SQL 推迟到微任务执行。而批量写入跑在同步的db.transaction(() { ... })回调里回调内无法await。如果语句用.execute()派发事务会在推迟的 Promise 落定前就提交写入变成静默 no-op——没有错误、没有行变更。因此同步事务回调内必须调用同步的.run(placeholderValues)// favorites 是 playlist 作用域的按 (contentId, playlistId) 过滤 // 否则另一个播放列表里相同 contentId 的收藏也会被改写。 const stmt db .update(schema.favorites) .set({ position: sqlnumber${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) { // 不是 .execute() stmt.run({ position, contentId: content_id, playlistId: playlist_id }); } });这个坑曾咬到reorderGlobalFavorites和removeRecentItemsBatchissue #1137自定义收藏拖拽顺序在“本播放列表”视图里静默地从未持久化全局“所有播放列表”收藏因路径还向appState的global-favorites-channel-order-v1键写顺序并在读取时重放掩盖了该问题。mock 单测也抓不到它——jest mock 记录.execute()调用与.run()调用看起来一样因此回归测试显式断言必须用.run()且不能用.execute()。10. 构建、打包与开发期重建规则10.1 构建脚本产出三个 bundleapps/electron-backend/build-worker.js 产出三个 worker bundleepg-parser.worker.ts→dist/apps/electron-backend/workers/epg-parser.worker.jsdatabase.worker.ts→dist/apps/electron-backend/workers/database.worker.jsplaylist-refresh.worker.ts→dist/apps/electron-backend/workers/playlist-refresh.worker.jsworker 构建还会别名iptvnator/shared/database/schema与iptvnator/shared/database/path-utils避免从 worker 内引入共享数据库 barrel——后者会拉进假设 Electron 主进程环境的运行时代码。10.2 路径解析优先级DatabaseWorkerClient解析 worker 路径的顺序开发路径__dirname下的workers/目录打包路径process.resourcesPath/dist/apps/electron-backend/workers/...回退打包路径path.dirname(app.getAppPath())。10.3 打包产物验证tools/packaging/verify-electron-package-layout.mjs 验证 Linux unpacked resources、macOS app bundles、Windows unpacked app resources 中的 worker 产物其workerFiles列表当前检查epg-parser.worker.js与database.worker.jsplaylist refresh bundle 已产出但尚未作为第三个显式检查项并检查better-sqlite3位于一个批准的 unpacked node_modules 位置。Snap 打包兼容性还包括snap.base core22与 X11 回退启动参数。10.4 开发期重建铁律运行中的 Electron 会一直使用启动时加载的 worker bundle——这是“修复看起来没生效”的最常见原因即便源码补丁是正确的。安全的工作流是pnpm nx test electron-backend pnpm nx run electron-backend:build-worker stat dist/apps/electron-backend/workers/database.worker.js # 确认产物时间戳然后重启 Electron再跑最接近的 Electron E2E 或 CDP 工作流。如果 preload、主进程或 web 输出也改了需要重建整个electron-backend目标CI1 NX_TASKS_RUNNER_DYNAMIC_OUTPUTfalse pnpm nx run electron-backend:build --skip-nx-cache stat -f %Sm %N dist/apps/electron-backend/workers/database.worker.js11. 调试与验证工具箱11.1 安全的 SQL 追踪绝不打印展开 SQL 与绑定值IPTVNATOR_TRACE_DB1与IPTVNATOR_TRACE_SQL1只输出固定的、白名单化的语句类型摘要经过共享脱敏逻辑 libs/shared/logging/src/lib/sql-trace-summary.ts 处理。该模块用正则从 SQL 文本提取首词SELECT/INSERT/UPDATE/DELETE等只有命中所允许类型集合的才会输出语句类型OTHER一律不输出展开的 SQL 文本与绑定值永远不会被打印。DB 传输追踪也保持独立脱敏。11.2 完整追踪环境变量IPTVNATOR_TRACE_STARTUP1宽启动追踪集合——BrowserWindow 生命周期、渲染进程桥调用、DB worker 请求/事件、SQL 追踪IPTVNATOR_TRACE_IPC1记录跨 preload 桥的window.electron.*方法调用IPTVNATOR_TRACE_DB1脱敏的DatabaseWorkerClient请求派发、完成耗时与DB_OPERATION_EVENT摘要IPTVNATOR_TRACE_SQL1仅输出白名单语句类型的共享主进程与 worker 连接追踪IPTVNATOR_TRACE_WINDOW1BrowserWindow 加载、导航、unresponsive、render-process-gone转换IPTVNATOR_TRACE_RENDERER_CONSOLE1把渲染进程 console 消息镜像到 Electron 终端输出。渲染进程路由在 DevTools 可用前就冻结时可这样启动IPTVNATOR_TRACE_STARTUP1 pnpm run serve:backend11.3 测试专用环境变量IPTVNATOR_DB_WORKER_BATCH_DELAY_MS20给每个可取消批检查点加入人为延迟让 E2E 时序确定。默认0时每个检查点仍会让出一个事件循环拍不增加定时器延迟使 worker 能在下一批开始前收到排队中的取消消息。仅测试用生产不得设置。IPTVNATOR_DB_WORKER_ROWS_PER_TRANSACTION100缩小目录写入预算让几千行的 mock 目录也能产生多次提交从而多个检查点与进度事件便于观察导入中途状态。未设置或非正整数时保持默认CONTENT_ROWS_PER_TRANSACTION 5,000。11.4 性能剖析开发/测试专用IPTVNATOR_PERF_WORKER_PROFILING1为每个数据库 worker 响应附带开发/测试专用性能元数据请求各阶段的 epoch 时间戳、worker 线程 CPU 用户/系统微秒process.threadCpuUsage()、工作区间的事件循环利用率、事件循环延迟 max/p95/p99来自请求自己的直方图。默认关闭生产启动必须保持关闭。同一开关还启用一次性performance:collect-post-gc-heap请求通过专属MessagePort传输worker 调用暴露的globalThis.gc()后读取自身 V8 isolate 堆统计。11.5 常用验证命令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 -- --projectelectron --grep Electron Xtream Responsiveness pnpm run verify:package-layout -- macos arm64 pnpm run verify:package-layout -- linux pnpm run verify:package-layout -- windows运行时验证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 arm6411.6 测试覆盖要点database-worker-client.spec.ts 覆盖worker ready → request → response 流程、事件转发到挂起请求、序列化 worker 错误传播、取消工作的AbortError传播、取消消息路由到存活 worker、worker 退出恢复与重新启动。worker-runtime-paths.spec.ts 覆盖打包/开发路径解析、打包原生模块搜索路径顺序、聚合的原生模块解析错误。xtream-responsiveness.e2e.ts 覆盖大型 Xtream 导入及时显示浮层、导入期间 DB worker 进度事件推进、导入/删除期间渲染进程动画帧持续、大型 Xtream 播放列表删除显示行级忙碌 UI 并干净完成。12. 扩展 worker新增重型操作的标准流程当需要新增一个重型 SQLite 操作时按以下清单执行来自 docs/architecture/sqlite-db-worker.md把 SQL 重逻辑放进 apps/electron-backend/src/app/database/operations/把通道名加到 database-worker.types.ts 的DB_WORKER_OPERATIONS在 database.worker.ts 的executeRequest中处理它通过DatabaseWorkerClient代理 IPC 处理器若渲染进程需要进度发出请求级DbOperationEvent尽量复用现有 preload/service API而不是创建新的渲染进程契约重跑 worker 单元测试与至少一次 Electron 运行时冒烟。13. 当前边界与局限以下事项被刻意排除在范围内把网络密集的 Xtream 抓取移出当前路径未做无证据表明会阻塞主进程时随意迁移轻量下载与 EPG 专属主进程处理器为批量破坏性操作提供更细粒度的删除进度全仓库范围的 Angular/Jest 清理针对当前失败的 web 测试基线。总结IPTVnator 的 SQLite DB Worker 是一次典型的“把重型 I/O 从 UI 线程请出去”的架构实践一条长驻worker_threads工作线程承接全部重型非 EPG SQLite 操作requestId管传输、operationId管业务请求级DB_OPERATION_EVENT取代全局进度事件协作式取消配合行预算事务在提交间隙落点同时用共享 pragma 让 EPG 与 DB 两条 worker 和平共存。无论是阅读 .codex/skills/iptvnator-sqlite-db-worker/SKILL.md 了解职责归属、深入 docs/architecture/sqlite-db-worker.md 追查设计决策还是直接阅读 database.worker.ts 与 database-worker-client.ts 的源码都能清晰还原这套从渲染进程到 SQLite 的完整数据通路。【免费下载链接】iptvnator:tv: Cross-platform IPTV player application with multiple features, such as support of m3u and m3u8 playlists, favorites, TV guide, TV archive/catchup and more.项目地址: https://gitcode.com/GitHub_Trending/ip/iptvnator创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考