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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user