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 ## Real-time / live updates
### Server-sent events for data changes ### Dual event bus architecture
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.
Server-sent events (SSE) are the natural fit — lightweight, one-directional, no Two independent buses, bridged by SSE:
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.
Ties directly into the delta tracking plan: the same write path that appends to the **Server bus** (`lib/bus.mjs`, Node `EventEmitter`):
delta log can also fan out to connected SSE clients. - 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 ## App architecture
@@ -69,10 +101,39 @@ const SECTION_RENDERERS = {
function render() { sync_nav(); SECTION_RENDERERS[section]?.(); } 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 monolith
`app.mjs` is large. Consider splitting into per-section modules `app.mjs` is large. Split into per-section view modules (`views/components.mjs`,
(`views/components.mjs`, `views/grids.mjs`, etc.) that each export their render `views/grids.mjs`, `views/bins.mjs`, etc.) each owning its local state, subscribing
function and own their local state. 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 ### Split CSS into per-section files
`style.css` is a single large file and getting hard to navigate. Split into `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 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 → exist — they all follow the same pattern of JS function → structured output →
context-specific renderer. 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 ## Search & views