Files
logseq/docs/agent-guide/074-db-worker-node-invoke-main-thread-refactor.md

319 lines
18 KiB
Markdown

# db-worker-node `invoke-main-thread` Removal and Worker-Local Refactor Plan
Goal: Remove all production `invoke-main-thread` usage from db worker logic and make `db-worker-node` fully self-contained for API execution.
Goal: Refactor each current main-thread API dependency into db-worker-owned implementations that work in both Node and Browser runtimes.
Goal: For UI-dependent interactions, replace direct worker->main-thread invocation with a request/response protocol where worker sends `postMessage`, and main thread actively calls db-worker thread APIs to respond.
Goal: Enforce request isolation with `request-id` across concurrent UI requests.
Architecture: Keep runtime-specific logic behind worker platform adapters (`frontend.worker.platform`, `frontend.worker.platform.node`, `frontend.worker.platform.browser`) and keep shared logic runtime-agnostic.
Related:
- `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/state.cljs`
- `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/ui_request.cljs`
- `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_worker_node.cljs`
- `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_worker.cljs`
- `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_core.cljs`
- `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/sync/assets.cljs`
- `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/sync/auth.cljs`
- `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/sync/crypt.cljs`
- `/Users/rcmerci/gh-repos/logseq/src/main/frontend/handler/assets.cljs`
- `/Users/rcmerci/gh-repos/logseq/src/main/frontend/handler/e2ee.cljs`
- `/Users/rcmerci/gh-repos/logseq/src/main/frontend/handler/user.cljs`
- `/Users/rcmerci/gh-repos/logseq/src/main/frontend/persist_db/browser.cljs`
- `/Users/rcmerci/gh-repos/logseq/src/main/frontend/handler/worker.cljs`
## Problem statement
`db-worker-node` currently sets a main-thread stub that always rejects (`main-thread is not available in db-worker-node`).
Historically, shared worker modules called `worker-state/<invoke-main-thread` for auth refresh, asset upload/download metadata, E2EE key/password operations, and search-idle checks.
That created runtime divergence:
- Browser worker could call main-thread APIs through Comlink.
- Node daemon could not, so behavior depended on fallback code quality or failed outright.
Target state: db worker owns all non-UI logic and uses only explicit message protocol for UI interactions.
## Progress snapshot (current)
### Completed migrations
- Auth refresh dependency on `:thread-api/ensure-id&access-token` was removed from worker sync auth path.
- Asset upload/download/metadata dependencies on main-thread thread APIs were removed and replaced by worker-local implementations through platform adapters.
- Search idle dependency on `:thread-api/input-idle?` was removed from `db_core`; it now uses a **main-thread push + worker TTL cache** model:
- main thread periodically pushes `:thread-atom/search-input-idle-status` with `{repo {:idle? ... :ts ...}}`
- worker consumes that state with a TTL window and falls back to non-blocking behavior when state is missing/stale.
- E2EE UI-interaction path in `frontend.worker.sync.crypt` was migrated to a worker-owned request manager:
- worker now emits `:db-worker/ui-request` with `request-id`
- main thread responds via `:thread-api/resolve-ui-request` / `:thread-api/reject-ui-request`
- worker crypt flow no longer directly calls `worker-state/<invoke-main-thread`
- pending UI requests are cancelled on close/shutdown paths (`close-db`, `db-sync-close-db`, `unsafe-unlink-db`, browser `stop-db-worker!`)
### Remaining direct `invoke-main-thread` production usages
- None in production worker modules.
- `frontend.worker.state/<invoke-main-thread` remains as legacy infrastructure, but has no active production call sites.
## Historical inventory of `invoke-main-thread` dependencies (baseline before migration)
| Call site | Current main-thread API | Category | Runtime risk in `db-worker-node` |
| --- | --- | --- | --- |
| `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/sync/auth.cljs` (`<resolve-ws-token`) | `:thread-api/ensure-id&access-token` | Auth/token refresh | Token refresh path may fail without worker-local refresh |
| `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/sync/assets.cljs` (`upload-remote-asset!`) | `:thread-api/rtc-upload-asset` | Asset I/O + network | Upload path depends on main-thread asset code |
| `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/sync/assets.cljs` (`download-remote-asset!`) | `:thread-api/rtc-download-asset` | Asset I/O + network | Download path depends on main-thread asset code |
| `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/sync/assets.cljs` (`request-asset-download!`) | `:thread-api/get-asset-file-metadata` | Asset metadata | Existence/checksum check depends on main thread |
| `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/sync/crypt.cljs` (`<native-save-password-text!`) | `:thread-api/native-save-e2ee-password` | Password persistence | Native keychain path unavailable in node daemon |
| `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/sync/crypt.cljs` (`<native-read-password-text`) | `:thread-api/native-get-e2ee-password` | Password persistence | Same |
| `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/sync/crypt.cljs` (`<native-delete-password-text!`) | `:thread-api/native-delete-e2ee-password` | Password persistence | Same |
| `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/sync/crypt.cljs` (`<generate-and-upload-user-rsa-key-pair!`) | `:thread-api/request-e2ee-password` | UI prompt | Requires explicit UI handshake |
| `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/sync/crypt.cljs` (`<decrypt-private-key`) | `:thread-api/decrypt-user-e2ee-private-key` | UI/session-dependent decrypt | Must be worker-local + UI fallback protocol |
| `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/sync/crypt.cljs` (`:thread-api/init-user-rsa-key-pair`) | `:thread-api/request-e2ee-password` | UI prompt | Same as above |
| `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_core.cljs` (`<wait-for-search-index-idle!`) | `:thread-api/input-idle?` | UI state query | Tight loop currently requires main-thread sync point |
## Design constraints and invariants
1. `db-worker-node` must never depend on main-thread APIs.
2. Shared worker code must run in both Node and Browser runtimes.
3. UI interactions must use explicit async protocol with `request-id` correlation.
4. Error shapes must remain stable (or be intentionally versioned) across transport layers.
5. Migration should be incremental with behavior-compatible phases.
## Target architecture
### 1) Remove worker-side direct main-thread invocation from business paths
Keep `frontend.worker.state/<invoke-main-thread` only as legacy infrastructure for browser compatibility during migration, then shrink/remove usage in production worker modules.
Acceptance target after migration:
- No production call sites to `<invoke-main-thread` outside temporary migration guards.
- `db-worker-node` main-thread stub remains reject-only and never hit in normal flows.
### 2) Move main-thread business logic into worker-owned modules
#### Auth refresh (`:thread-api/ensure-id&access-token`)
- Move token refresh implementation into worker sync auth module.
- Reuse current refresh token and auth state from worker state (`:auth/id-token`, `:auth/refresh-token`) that is already synchronized.
- Keep main thread responsible only for UI/session orchestration, not worker internal token lifecycle.
#### Asset operations (`:thread-api/rtc-upload-asset`, `:thread-api/rtc-download-asset`, `:thread-api/get-asset-file-metadata`)
- Introduce worker-side asset I/O + transfer module (shared, runtime-agnostic).
- Move file read/write/checksum/upload/download logic from main-thread handler into worker with platform adapter calls.
- Keep progress events emitted from worker via existing broadcast channel.
#### E2EE password storage (`native-save/get/delete`)
- Replace main-thread keychain dependency with worker platform capabilities.
- Add explicit storage abstraction in platform adapters for encrypted password persistence.
- Keep existing encrypted payload format unchanged where possible.
#### Private key decrypt (`:thread-api/decrypt-user-e2ee-private-key`)
- Make worker-local decrypt path primary.
- Request UI password only when worker-local credentials are unavailable.
### 3) UI request/response protocol (request-id isolated)
For UI-dependent operations, worker should not call main-thread thread APIs directly.
Use this flow:
```text
1) Worker needs UI input
-> postMessage event to main thread
{:type :db-worker/ui-request
:request-id <uuid-v4>
:action <keyword>
:payload <map>
:timeout-ms 60000}
2) Main thread handles UI action (prompt/dialog/user interaction)
3) Main thread actively calls db-worker thread API:
:thread-api/resolve-ui-request [request-id result]
or
:thread-api/reject-ui-request [request-id error]
4) Worker resolves or rejects the pending promise by request-id.
```
Required worker internals:
- `*ui-requests-in-flight` map keyed by `request-id`.
- `request-id` is generated as UUID v4.
- default request timeout is `60000ms` (60s), with optional per-action override.
- timeout handling + cleanup.
- duplicate/late response protection.
- cancellation support for graph switch or worker shutdown.
Required acceptance:
- concurrent requests do not cross-resolve.
- timed-out request cannot be resolved later.
- all terminal states remove map entries.
#### UI interaction contract: headless vs interactive
The implementation uses two explicit modes instead of implicit fallback behavior.
- **Headless mode (default for `db-worker-node`/CLI):**
- If an operation requires UI input and no interactive channel is available, return a typed error.
- Error code should be stable and machine-readable: `:ui-interaction-required`.
- Error payload should include at least `:action` and optional `:hint` (for example, configured password fallback).
- **Interactive mode (browser app):**
- Worker emits `:db-worker/ui-request` with `request-id`.
- Main thread performs UI interaction and actively calls worker thread APIs to resolve or reject the request.
- Worker resumes flow only when matching `request-id` is resolved.
This dual contract keeps headless behavior deterministic while preserving async interactive UX in browser runtime.
### 4) Platform abstraction updates for Node/Browser parity
Extend `frontend.worker.platform` contract with only capabilities required by migrated APIs.
Likely additions:
- secure encrypted secret storage (or equivalent persisted encrypted blob)
- binary asset read/write/stat helpers
- HTTP transfer helpers with progress hooks (if not already available in shared runtime)
- monotonic clock/timer helpers for timeout handling
Node implementation: `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/platform/node.cljs`.
Browser implementation: `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/platform/browser.cljs`.
## Per-API migration matrix
| Legacy main-thread API | New worker-owned implementation | UI request protocol needed? | Node/Browser notes |
| --- | --- | --- | --- |
| `:thread-api/ensure-id&access-token` | Worker-local refresh function in sync auth module | No | Must work without UI in node daemon |
| `:thread-api/rtc-upload-asset` | Worker asset upload API | No | Use platform file + HTTP adapters |
| `:thread-api/rtc-download-asset` | Worker asset download API | No | Same |
| `:thread-api/get-asset-file-metadata` | Worker metadata/checksum API | No | Same |
| `:thread-api/native-save-e2ee-password` | Worker secure persistence API | No | Use platform adapter implementations |
| `:thread-api/native-get-e2ee-password` | Worker secure read API | No | Same |
| `:thread-api/native-delete-e2ee-password` | Worker secure delete API | No | Same |
| `:thread-api/request-e2ee-password` | Worker emits UI request and waits on `resolve-ui-request` | Yes | Node can return typed `:ui-interaction-required` unless configured fallback exists |
| `:thread-api/decrypt-user-e2ee-private-key` | Worker-local decrypt + optional UI request for password | Yes (fallback) | Keep headless path for node |
| `:thread-api/input-idle?` | Main thread pushes idle-state updates; worker consumes a local TTL cache for indexing decisions | Yes | Push+TTL is the default model to avoid high-frequency request storms |
## Implementation phases
### Phase 0: Safety rails and observability
1. Add explicit metric/log points for any call to `<invoke-main-thread` in worker code.
2. Add request-id propagation fields to db-worker-node invoke/event logs for traceability.
3. Add temporary feature flag to switch between old and new path per migrated domain.
### Phase 1: Non-UI API migration
1. Migrate auth refresh from main thread to worker.
2. Migrate asset metadata/upload/download to worker module + platform adapters.
3. Migrate E2EE encrypted password persistence to worker platform APIs.
4. Keep compatibility shims temporarily, but default to worker-local path.
Exit criteria:
- No non-UI flow depends on main thread.
- CLI and db-worker-node sync paths pass regression tests.
### Phase 2: UI request protocol
1. Introduce generic worker UI request manager (`request-id`, timeout, cleanup).
2. Add worker thread APIs for resolving/rejecting UI requests.
3. Add main-thread handler wiring in `frontend.handler.worker` for `:db-worker/ui-request` events.
4. Route E2EE prompt/decrypt UI-dependent flows through new protocol.
Exit criteria:
- concurrent password requests are isolated by `request-id`.
- graph switch/shutdown does not leak pending requests.
### Phase 3: Search idle and remaining UI-state dependencies
1. Replace `:thread-api/input-idle?` direct invocation with **main-thread push + worker TTL cache** model.
2. Define TTL window and stale-state behavior for indexing loops.
3. Validate search indexing throughput and UI responsiveness.
Exit criteria:
- no direct worker->main-thread API invocation in search flow.
- push+TTL idle-state model is active and covered by tests.
- no regression in search build behavior.
### Phase 4: Remove legacy dependencies
1. Remove remaining production usages of `worker-state/<invoke-main-thread` in worker sync/core modules.
2. Update `non-repo-methods` and API inventory if method ownership changed.
3. Keep browser compatibility wrappers only if still required by bootstrapping internals.
Final exit criteria:
- `invoke-main-thread` usage in production code is zero (or explicitly limited to bootstrap-only internals with no business logic).
- `db-worker-node` full API suite works without main-thread API dependencies.
## Tests and verification plan
### Unit tests
- Worker auth tests: token refresh in worker without main-thread stubs.
- Worker asset tests: metadata/upload/download via worker module and platform mocks.
- Worker crypt tests: password storage/decrypt paths do not call main thread.
- UI request manager tests: request-id isolation, timeout, duplicate response rejection.
### Integration tests
- Browser flow: UI request roundtrip (`postMessage` -> main thread -> `resolve-ui-request`).
- Node flow: same API paths run headless and return typed errors when UI is mandatory.
- Search indexing flow: idle-state protocol behavior under repeated checks.
### Regression checks
- `eca__grep` gate: no production `worker-state/<invoke-main-thread` call sites in migrated modules.
- `db-worker-node` daemon invoke smoke tests for sync + e2ee + asset paths.
- Existing CLI and worker tests remain green.
## Risks and mitigations
1. **E2EE flow regressions**
- Mitigation: migrate with feature flags and run dedicated crypt regression suite first.
2. **Asset transfer behavior drift**
- Mitigation: keep payload format/checksum semantics unchanged during extraction.
3. **Request map leaks / stale requests**
- Mitigation: enforce timeout + shutdown cleanup + finalizer assertions.
4. **Search performance impact**
- Mitigation: add request throttling and local TTL cache for idle-state checks.
5. **Protocol mismatch between main thread and worker**
- Mitigation: define strict message schema and validate fields at both ends.
## Acceptance criteria
1. Every current production `invoke-main-thread` dependency listed in this plan has a worker-owned replacement.
2. `db-worker-node` runs without any required main-thread API bridge.
3. UI-dependent flows use `postMessage` request + main-thread active callback to worker thread API.
4. `request-id` enforces isolation for concurrent UI requests.
5. In headless mode, UI-required operations return typed error `:ui-interaction-required` with stable machine-readable payload.
6. Node and Browser runtimes both pass targeted tests for migrated paths.
7. No unresolved pending UI requests remain after graph close or worker shutdown.
## Suggested execution order
1. Auth + non-UI crypt storage.
2. Asset API migration.
3. Generic UI request protocol.
4. E2EE prompt/decrypt migration.
5. Search idle-state migration.
6. Legacy call-site removal and cleanup.
## Confirmed decisions
1. `request-id` format: UUID v4.
2. Default UI request timeout: 60s (`60000ms`), optional per-action override.
3. Search idle state model: main-thread push + worker TTL cache.
4. Headless behavior: UI-required operations return typed error `:ui-interaction-required`.
5. Execution order: follow the phased order defined in this document.
## Notes
This plan intentionally prioritizes behavior parity and migration safety over immediate deep refactor of all related modules.
The protocol and platform abstraction should be stabilized first; broad code movement should follow once test harnesses are in place.