Update future-plans: dual event bus, UI sub-project boundary, fix duplicate

- Replace simple SSE note with full dual-bus architecture:
  server EventEmitter + client pub/sub, SSE as bridge, effects/ pattern,
  mutation wrapper to avoid wildcard, collection-level event granularity
- Add 'UI as self-contained sub-project' section: composition root pattern,
  main.mjs vs mock-main.mjs entry points, mock-api contract, views-never-
  call-each-other discipline
- Expand app.mjs monolith note to mention mount() export pattern
- Remove duplicate CodeMirror paragraph (copy-paste artifact)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-03 14:12:28 +00:00
parent 72897c5b2d
commit 5fe7273e35

View File

@@ -23,20 +23,52 @@ undo, audit trails, and debugging data corruption.
## Real-time / live updates
### Server-sent events for data changes
When data changes on the server (upload, edit, delete — from any client), connected
browsers should receive a notification and update the affected view automatically.
Use case: uploading photos from a phone while the desktop browser has the Images or
Bins section open.
### Dual event bus architecture
Server-sent events (SSE) are the natural fit — lightweight, one-directional, no
library needed. The server emits a change event with a `type` (e.g. `source_images`,
`bins`) and the client re-fetches and re-renders only the affected collection.
Views that aren't currently visible don't need to do anything — they'll reload on
next navigation.
Two independent buses, bridged by SSE:
Ties directly into the delta tracking plan: the same write path that appends to the
delta log can also fan out to connected SSE clients.
**Server bus** (`lib/bus.mjs`, Node `EventEmitter`):
- Route handlers emit a specific event after each mutation (`field:deleted`,
`bin:changed`, etc.) and nothing else — side effects are not inline
- Effect modules in `server/effects/` subscribe and handle cascading work:
- `field-effects.mjs` — strips deleted field from all components/bins/bin types
- `sse-effects.mjs` — broadcasts mutations to connected SSE clients
- `audit-effects.mjs` — writes delta log (future)
- `bin-effects.mjs` — e.g. propagates type dimension changes to bins
- New cross-cutting concerns (audit, cache invalidation, notifications) are
additional listeners — route handlers never grow
**Client bus** (`lib/bus.mjs`, lightweight pub/sub or `EventTarget`):
- `api.mjs` emits on the bus after every successful mutation
- `sse.mjs` (SSE connection client) translates incoming server events to bus emits
- View modules subscribe to relevant events and re-render; they never call each other
- `mock-api.mjs` also emits on the same bus after in-memory mutations, so views
react correctly in mock mode without any SSE
**SSE bridge**: `sse-effects.mjs` on the server broadcasts to connected clients;
`sse.mjs` on the client receives and re-emits on the client bus. Views are unaware
of whether a change was local or remote.
**Avoiding wildcard listeners**: instead of a wildcard `*` listener (not natively
supported by `EventEmitter`), emit a generic `mutation` event alongside every
specific event. The SSE broadcaster listens to `mutation`; everything else listens
to specific events. New event types are automatically forwarded without touching
the broadcaster.
```js
function emit(event, data) {
bus.emit(event, data);
bus.emit('mutation', { event, data });
}
```
**Event granularity**: collection-level events are sufficient (`bins:changed`,
`components:changed`). Passing the affected id or record is optional — views can
use it to do a targeted update or ignore it and re-fetch the collection. Fine-grained
events are an optimisation to add later if full-collection re-fetches become slow.
Ties into the delta tracking plan: `audit-effects.mjs` is another bus listener —
the same mutation path that drives SSE also drives the delta log.
## App architecture
@@ -69,10 +101,39 @@ const SECTION_RENDERERS = {
function render() { sync_nav(); SECTION_RENDERERS[section]?.(); }
```
### UI as a self-contained sub-project
The UI boundary is `api.mjs` — every piece of data the UI touches goes through
named exports in that file. This seam should be made explicit so the UI can be
developed and tested against a mock without a running server.
**Composition root / dependency injection**: `app.mjs` should not import `api.mjs`
directly. Instead it receives the api implementation as a parameter. Two thin entry
files wire it up:
```
main.mjs — imports real api.mjs, passes to app.start()
mock-main.mjs — imports mock-api.mjs, passes to app.start()
```
`mock-main.mjs` is a separate deployable (e.g. served at `/mock` or on a dev port),
not a URL flag. The app has no runtime knowledge of which implementation it received.
**mock-api.mjs**: same exports as `api.mjs`, backed by in-memory arrays seeded with
realistic fixture data. Mutations update the in-memory state so the UI behaves
realistically (add/delete/edit all persist within the session). Also emits on the
client bus so cross-view reactivity works identically to the real app. No SSE
connection needed in mock mode — the bus events come from the mock mutations.
**Views never call each other**: once split into modules, `views/bins.mjs` must
not import `views/inventory.mjs`. Cross-section reactions happen exclusively through
the client bus. This is the main structural discipline that makes the split work.
### app.mjs monolith
`app.mjs` is large. Consider splitting into per-section modules
(`views/components.mjs`, `views/grids.mjs`, etc.) that each export their render
function and own their local state.
`app.mjs` is large. Split into per-section view modules (`views/components.mjs`,
`views/grids.mjs`, `views/bins.mjs`, etc.) each owning its local state, subscribing
to bus events at init, and exporting a single `mount(container)` function. The
composition root (`main.mjs`) imports all view modules and registers them.
### Split CSS into per-section files
`style.css` is a single large file and getting hard to navigate. Split into
@@ -328,10 +389,6 @@ slot targets) in addition to custom formatters.
This revision also applies to field parsers and search view expressions once those
exist — they all follow the same pattern of JS function → structured output →
context-specific renderer.
Any field that accepts JavaScript (name formatter templates, future custom search
views, field parsers, etc.) should use a CodeMirror 6 editor instead of a plain
`<textarea>`. Gives syntax highlighting, bracket matching, and a proper editing
experience for JS snippets.
## Search & views