From 5fe7273e351597f8a84236a0536edf554fb24622 Mon Sep 17 00:00:00 2001 From: mikael-lovqvists-claude-agent Date: Fri, 3 Apr 2026 14:12:28 +0000 Subject: [PATCH] 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 --- future-plans.md | 95 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 76 insertions(+), 19 deletions(-) diff --git a/future-plans.md b/future-plans.md index dc658f7..b5a4d39 100644 --- a/future-plans.md +++ b/future-plans.md @@ -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 -`