Initial import of LogDarioSeq baseline
Establishes version control for the personal Logseq DB fork before starting the server-client architecture migration and domain features described in docs/agent-guide and the approved implementation plan. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
260
docs/adr/0021-rfx-managed-frontend-state.md
Normal file
260
docs/adr/0021-rfx-managed-frontend-state.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# ADR 0021: RFX-managed Frontend State
|
||||
|
||||
Date: 2026-06-03
|
||||
Status: Accepted
|
||||
|
||||
## Context
|
||||
|
||||
ADR 0019 replaced Rum with HSX and moved the UI runtime onto React 19. The
|
||||
frontend now has a React-compatible component runtime, but application state is
|
||||
still managed by `frontend.state` through a large global atom plus many nested
|
||||
atoms.
|
||||
|
||||
The current state layer has several responsibilities mixed together:
|
||||
|
||||
- `frontend.state/state` stores the main application map.
|
||||
- Many values inside that map are themselves atoms, for example editor,
|
||||
selection, RTC, PDF, command palette, scroll, and other high-churn UI refs.
|
||||
- `frontend.state/use-sub` provides React subscriptions by wrapping the global
|
||||
atom or a nested atom with `logseq.shui.hooks/use-atom`.
|
||||
- `frontend.state/get-state`, `set-state!`, and `update-state!` are used across
|
||||
handlers, components, workers, plugins, mobile code, and Electron integration.
|
||||
- `frontend.state/pub-event!` publishes vectors to a `core.async` channel stored
|
||||
at `:system/events`.
|
||||
- `frontend.handler.events` consumes that channel and routes events through a
|
||||
`defmulti`.
|
||||
- Worker DB queries and component subscriptions use a separate reactive layer in
|
||||
`frontend.db.react` and `frontend.db.hooks`.
|
||||
- Several subsystems use Missionary flows or direct atom watches for runtime
|
||||
state that is not just component state.
|
||||
|
||||
This makes UI reactivity explicit enough for React hooks, but the ownership is
|
||||
still split between path cursors, nested atoms, a global event channel, and a
|
||||
large set of imperative helpers. A code scan showed the size of the migration
|
||||
surface:
|
||||
|
||||
- about 178 `state/use-sub` call sites under `src/main/frontend`;
|
||||
- about 310 `set-state!` or `update-state!` call sites under
|
||||
`src/main/frontend`;
|
||||
- about 190 `pub-event!` call sites under `src/main/frontend`;
|
||||
- about 102 `frontend.handler.events/handle` methods.
|
||||
|
||||
RFX is a better fit for the next step because it keeps the re-frame style
|
||||
architecture while using React Context and hooks instead of Reagent. It provides
|
||||
scoped app instances, `use-sub`, `dispatch`, `reg-sub`, `reg-event-db`,
|
||||
`reg-event-fx`, effects, coeffects, snapshots, and a queue. Logseq has already
|
||||
forked RFX and added root `deps.edn` support on `logseq/rfx` branch
|
||||
`logseq/fork`, commit `d37aaceb37fcaf969c5cd04646dfdb985d65d3c2`.
|
||||
|
||||
The migration must not destabilize editor input, block trees, DB worker
|
||||
transactions, RTC, plugin APIs, Electron IPC, mobile native flows, or
|
||||
DataScript query subscriptions.
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt RFX as the frontend application state and event runtime.
|
||||
|
||||
Add RFX to the main frontend dependency root from the Logseq fork:
|
||||
|
||||
```clojure
|
||||
io.github.logseq/rfx
|
||||
{:git/url "https://github.com/logseq/rfx.git"
|
||||
:git/sha "d37aaceb37fcaf969c5cd04646dfdb985d65d3c2"}
|
||||
```
|
||||
|
||||
Create a Logseq-owned RFX context, store, and registry. The context should be
|
||||
provided near the root render in `frontend.core`, around `page/current-page`.
|
||||
The RFX instance must be initialized during frontend startup before event
|
||||
handling starts.
|
||||
|
||||
Replace `frontend.state` as the state owner in one coordinated migration.
|
||||
Existing non-hook public entry points may remain as thin call-through APIs only
|
||||
when they preserve caller compatibility without keeping state outside RFX:
|
||||
|
||||
- `get-state`
|
||||
- `set-state!`
|
||||
- `update-state!`
|
||||
- `pub-event!`
|
||||
- focused helpers such as `get-current-repo`, `set-current-repo!`,
|
||||
`get-config`, editor helpers, selection helpers, and RTC helpers
|
||||
|
||||
Replace all `frontend.state/use-sub` and focused hook call sites with
|
||||
`rfx/use-sub` or Logseq-owned domain hooks backed by RFX subscriptions. Do not
|
||||
keep `frontend.state/use-sub` as a compatibility wrapper. `get-state`,
|
||||
`set-state!`, `update-state!`, and focused non-hook helpers should read
|
||||
snapshots or dispatch RFX events instead of mutating the old global atom.
|
||||
|
||||
Use one source of truth:
|
||||
|
||||
- Frontend application state is plain data in the RFX app db and is updated
|
||||
through RFX events.
|
||||
- Nested atoms must not remain as the representation for application state.
|
||||
Convert nested atom state to plain values and path updates during the
|
||||
migration.
|
||||
- Missionary flows and direct atom watches must not remain as state propagation
|
||||
paths for frontend application state. Replace them with RFX subscriptions,
|
||||
events, effects, and coeffects.
|
||||
- Imperative resources such as DOM nodes, editor handles, timers, external
|
||||
worker objects, Electron handles, and plugin runtime handles stay outside the
|
||||
RFX app db. They may be held in refs only when they are not renderable
|
||||
application state and do not drive UI subscriptions directly.
|
||||
|
||||
Migrate every frontend state domain and its event handlers in the same change.
|
||||
Do not land partial domain migrations. State-only migrations that still rely on
|
||||
`frontend.handler.events/handle` for writes are rejected, and event-only
|
||||
migrations whose render state still lives behind legacy path cursors are also
|
||||
rejected.
|
||||
|
||||
`pub-event!` may remain as the public promise-returning compatibility API for
|
||||
callers, but it should dispatch into RFX instead of writing to `:system/events`.
|
||||
It must route to explicit `rfx/reg-event-db` or `rfx/reg-event-fx`
|
||||
registrations and resolve or reject the deferred promise from the RFX event
|
||||
result. Do not keep a bridge that invokes the existing
|
||||
`frontend.handler.events/handle` multimethod.
|
||||
|
||||
Use RFX to replace flow-based frontend state propagation. DataScript execution
|
||||
and worker-owned invalidation can remain worker responsibilities, but
|
||||
UI-facing query result state and refresh orchestration should be exposed through
|
||||
RFX subscriptions and events instead of Missionary flows or atom watches.
|
||||
|
||||
## Migration Shape
|
||||
|
||||
The migration should be one coordinated replacement of the frontend state and
|
||||
event runtime.
|
||||
|
||||
1. Add the RFX dependency and create a small `frontend.rfx` namespace that owns
|
||||
the app context, registry, initialization, dispatch helpers, and root
|
||||
provider.
|
||||
2. Move the initial plain app state map construction out of the `defonce state`
|
||||
expression into a reusable function.
|
||||
3. Move all frontend application state domains to plain RFX app-db values,
|
||||
including route state, graph/current repo state, search, modals, sidebars,
|
||||
theme flags, auth tokens, import/loading state, editor state, selection,
|
||||
command palette, PDF, RTC UI state, plugin UI state, Electron UI state, and
|
||||
mobile UI state.
|
||||
4. Replace all `frontend.handler.events/handle` methods with explicit
|
||||
`rfx/reg-event-db` or `rfx/reg-event-fx` registrations. Keep event ids
|
||||
domain-scoped and register effects/coeffects for DB worker calls, RTC,
|
||||
plugins, Electron IPC, mobile native calls, and async error capture.
|
||||
5. Replace `:system/events` with RFX dispatch behind `pub-event!` and remove the
|
||||
`core.async` event channel.
|
||||
6. Replace Missionary flows and direct atom watches that propagate frontend
|
||||
application state with RFX subscriptions, events, effects, and coeffects.
|
||||
7. Replace all `frontend.state/use-sub` and focused state hook call sites with
|
||||
`rfx/use-sub` or domain hooks backed by `rfx/reg-sub`.
|
||||
8. Remove nested atoms, path cursors, and legacy hook paths for application
|
||||
state. Keep refs only for imperative resources that do not represent
|
||||
renderable state.
|
||||
9. Update tests in the same change so every moved domain has coverage for state
|
||||
reads, event-driven writes, async effects, and promise resolution.
|
||||
|
||||
Each domain should have a clear owner namespace, subscription ids, event ids,
|
||||
effects, coeffects, and test coverage.
|
||||
|
||||
## Consequences
|
||||
|
||||
State ownership becomes explicit. Components subscribe through React-safe RFX
|
||||
hooks, while non-view code can use RFX snapshots and dispatch against the same
|
||||
instance.
|
||||
|
||||
Tests and isolated UI surfaces can create scoped RFX contexts instead of
|
||||
mutating the global `frontend.state/state` atom. This matches the context model
|
||||
introduced by the HSX migration.
|
||||
|
||||
The event system gains a uniform event/effect/coeffect shape and immediately
|
||||
replaces the `core.async` channel plus `defmulti` dispatch path.
|
||||
|
||||
The migration cannot be a mechanical atom swap. Nested atom mutation does not
|
||||
automatically notify RFX subscriptions. State must move to plain values, and
|
||||
writes must move to RFX events.
|
||||
|
||||
Compatibility wrappers may remain only as public API aliases that call into RFX.
|
||||
They must not retain separate storage, separate event dispatch, Missionary flow
|
||||
state propagation, or direct atom-watch state propagation.
|
||||
|
||||
RFX effects that run async work must preserve existing error reporting behavior,
|
||||
including `:capture-error` and promise rejection paths used by callers of
|
||||
`pub-event!`.
|
||||
|
||||
## Rejected Alternatives
|
||||
|
||||
### Keep `frontend.state` As-is
|
||||
|
||||
This avoids migration cost but leaves Logseq with a custom subscription system
|
||||
after the React 19 and HSX migration. It also keeps event routing separate from
|
||||
state updates and does not give isolated RFX contexts for tests or component
|
||||
development.
|
||||
|
||||
### Replace Only `pub-event!`
|
||||
|
||||
Moving events to RFX while leaving render state on path cursors would improve
|
||||
event structure, but components would still subscribe through the custom atom
|
||||
layer. It would not solve the state ownership problem.
|
||||
|
||||
### Migrate Domain-by-Domain
|
||||
|
||||
An incremental domain migration would leave two state and event runtimes active
|
||||
for too long. That would preserve the current split ownership across RFX,
|
||||
legacy path cursors, Missionary flows, atom watches, the `core.async` event
|
||||
channel, and `defmulti` handlers. The migration should replace all frontend
|
||||
application state domains and handlers together.
|
||||
|
||||
### Migrate State Without Event Handlers
|
||||
|
||||
Moving state keys to RFX while keeping their writes in
|
||||
`frontend.handler.events/handle` would preserve the current split ownership in a
|
||||
new form. RFX-managed state must be written through RFX events, so state and
|
||||
event handlers should move together.
|
||||
|
||||
### Keep `frontend.state/use-sub` As A Wrapper
|
||||
|
||||
Keeping `frontend.state/use-sub` as a compatibility hook would preserve the old
|
||||
subscription boundary and make it harder to see which code is using RFX
|
||||
subscriptions directly. All hook call sites should move to `rfx/use-sub` or
|
||||
domain hooks backed by RFX.
|
||||
|
||||
### Keep Missionary Flows For Frontend State
|
||||
|
||||
Keeping Missionary flows or atom watches as frontend state propagation would
|
||||
leave RFX as only a partial store. Worker internals can still use their own
|
||||
runtime mechanisms, but UI-facing state should be expressed through RFX
|
||||
subscriptions and events.
|
||||
|
||||
### Keep DataScript Query Reactivity Outside RFX
|
||||
|
||||
Keeping `frontend.db.react` and `frontend.db.hooks/use-query` as a separate
|
||||
UI-facing subscription layer would leave another flow-like state runtime beside
|
||||
RFX. Query execution and worker invalidation may stay in the DB worker, but the
|
||||
frontend-facing subscription and refresh contract should be mediated through
|
||||
RFX.
|
||||
|
||||
## Verification
|
||||
|
||||
Before accepting the migration, verify:
|
||||
|
||||
- root rendering is wrapped in the Logseq RFX context;
|
||||
- no `frontend.state/use-sub` call sites remain;
|
||||
- direct `rfx/use-sub` and domain hook call sites obey React hook ordering;
|
||||
- RFX-managed keys re-render components after `set-state!`, `update-state!`,
|
||||
and event handlers update them;
|
||||
- all frontend application state domains update through RFX events, with no
|
||||
remaining legacy `defmethod handle` write path;
|
||||
- no Missionary flow or direct atom watch remains as a frontend application
|
||||
state propagation path;
|
||||
- non-managed refs are limited to imperative resources and do not drive UI
|
||||
subscriptions directly;
|
||||
- `pub-event!` still returns a promise that resolves or rejects with the event
|
||||
result;
|
||||
- event errors are logged and reported through the existing capture path;
|
||||
- graph switching, route changes, theme toggles, sidebars, modals, settings,
|
||||
search, import progress, login state, plugin UI, Electron IPC, mobile tabs,
|
||||
RTC indicator, editor editing, and block selection keep prior behavior;
|
||||
- UI-facing DB query result subscriptions and refresh orchestration go through
|
||||
RFX, while worker-owned query execution and invalidation keep prior behavior;
|
||||
- no React hook-order warnings, invalid element warnings, or missed subscription
|
||||
updates appear in Chrome;
|
||||
- focused unit tests cover every state domain, event registration, effect,
|
||||
coeffect, non-hook compatibility wrapper, and replaced flow path;
|
||||
- `bb dev:lint-and-test` passes;
|
||||
- relevant app or CLI E2E tests pass for graph switch, editor input, selection,
|
||||
sidebar, settings, plugin, and sync surfaces touched by the migrated keys.
|
||||
Reference in New Issue
Block a user