feat: add native module realtime transport
This commit is contained in:
@@ -70,7 +70,7 @@ Every module-facing contract in this platform is written so that **full Ekklesia
|
||||
thing that can satisfy it**. The intended second implementation is a reusable single-module
|
||||
standalone host — conceptually `@ekklesia/standalone-host` — which loads exactly one module and
|
||||
supplies the same host contracts Ekklesia does: configuration, secrets, object storage, HTTP, module
|
||||
lifecycle, and later realtime and display/UI hosting.
|
||||
lifecycle, and realtime, with display/kiosk hosting still later.
|
||||
|
||||
**No such package exists yet, and this pass does not create one.** It is recorded here because it is
|
||||
what several contract decisions are *for*, and because a change that quietly makes it unimplementable
|
||||
@@ -84,6 +84,12 @@ the scoped facade, binding resolution, delivery URLs and their bounds, lifecycle
|
||||
host-neutral, and a standalone host implements one interface to get all of it, including direct
|
||||
browser delivery ([ADR 0031](decisions/0031-direct-object-delivery.md)).
|
||||
|
||||
Realtime is the second worked example. A module declares local state channels and publishes opaque
|
||||
JSON through scoped contracts; its browser UI subscribes through another scoped contract. Neither
|
||||
side names SSE, EventSource, a platform URL, an origin, a cookie, or a module namespace. Ekklesia's
|
||||
host adapter uses permission-gated SSE, while a standalone host may choose another concrete
|
||||
transport without changing the module. See [ADR 0032](decisions/0032-native-module-realtime-transport.md).
|
||||
|
||||
### Storage servers are shared infrastructure, never per-module
|
||||
|
||||
- A module hosted under Ekklesia uses **Ekklesia's** storage service, through its own bindings.
|
||||
@@ -280,17 +286,17 @@ by its own esbuild script, that discovers installed module UIs and mounts them.
|
||||
|
||||
The missing piece was discovery. `GET /api/v1/module-ui` — the collection root of the same prefix the
|
||||
artifact route already owns — returns, per publishable module, its id and version, the server-produced
|
||||
artifact URL, the server-produced root of the module's own API, and the subset of that module's
|
||||
declared permissions the caller actually holds. Nothing else: no filesystem paths, and no display or
|
||||
navigation metadata, since ADR 0021 keeps Dashboard grouping an administrator's presentation
|
||||
configuration. The module set is the publication registry, never a rescan, so the inventory and the
|
||||
artifact route cannot disagree. It requires `core.modules.ui.read` (baseline `authenticated`) because
|
||||
it is precisely the enumeration ADR 0025 withheld from the anonymous artifact route.
|
||||
artifact URL, the server-produced roots of the module's own HTTP and realtime APIs, and the subset of
|
||||
that module's declared permissions the caller actually holds. Nothing else: no filesystem paths, and
|
||||
no display or navigation metadata, since ADR 0021 keeps Dashboard grouping an administrator's
|
||||
presentation configuration. The module set is the publication registry, never a rescan, so the
|
||||
inventory and artifact route cannot disagree. It requires `core.modules.ui.read` (baseline
|
||||
`authenticated`) because it is precisely the enumeration ADR 0025 withheld from the anonymous
|
||||
artifact route. ADR 0032 later extended ADR 0026's original descriptor with the realtime root.
|
||||
|
||||
Both URLs being server-produced is the load-bearing part. `moduleUiArtifactUrl` and the newly exported
|
||||
`moduleApiBaseUrl` are each the single construction of their URL shape, so the browser is a consumer
|
||||
of both — it never rebuilds `/api/v1/modules/<id>`, the prefix ADR 0022 forbids a module UI from
|
||||
spelling, and neither prefix is pinned by having a second author.
|
||||
All three URLs being server-produced is load-bearing. `moduleUiArtifactUrl`, `moduleApiBaseUrl`, and
|
||||
`moduleRealtimeBaseUrl` are each the single construction of their URL shape, so the browser consumes
|
||||
them rather than rebuilding a platform module prefix, and none is pinned by having a second author.
|
||||
|
||||
The shell builds a `ModuleUiHostContext` entirely from server-derived data, resolves a module's
|
||||
relative API calls onto its own API root (refusing anything that escapes it), and hands the artifact
|
||||
@@ -438,3 +444,30 @@ configures nothing. A reverse proxy in front of storage must pass the original `
|
||||
unchanged; it may terminate TLS, because the scheme is not signed. Streaming through a module's own
|
||||
route remains available for responses that must be computed or authorized per byte, and signed
|
||||
**write** URLs are deliberately not offered.
|
||||
|
||||
Phase 7A pass 5 adds native module realtime
|
||||
([ADR 0032](decisions/0032-native-module-realtime-transport.md)). Registration declares a bounded,
|
||||
module-local channel and one permission owned by that module; live runtime and HTTP contexts receive
|
||||
only that module's publisher. Every channel must publish an authoritative initial state before
|
||||
startup completes. Core serializes opaque JSON once, assigns a process-local per-channel sequence,
|
||||
and atomically installs each subscriber with the latest snapshot, eliminating a GET-then-subscribe
|
||||
race. It retains only the latest channel state and one coalescing pending update per client, so a
|
||||
slow browser never blocks module work or creates an unbounded queue. There is no history, replay,
|
||||
broker namespace, or durable state.
|
||||
|
||||
Ekklesia's concrete host transport is one-way Server-Sent Events because the demonstrated
|
||||
Announcements consumer sends complete state server-to-browser while browser commands already fit
|
||||
authenticated module HTTP. The channel permission is evaluated through the existing cookie
|
||||
principal and authorization system before allocation; the host closes streams after five minutes so
|
||||
reconnect re-evaluates that authority. Native EventSource owns recoverable reconnects, each of which
|
||||
starts with a fresh snapshot. The wire sends 15-second comment keepalives, disables common proxy
|
||||
buffering, carries no query token or replay id, and never exposes transport objects through the
|
||||
module UI SDK.
|
||||
|
||||
Shutdown closes long-lived subscriptions before finite HTTP drain, while leaving publishers usable
|
||||
for requests already in flight. Reverse-order stop then deactivates each module's publisher
|
||||
immediately before that module's own stop hook, so dependencies remain operational while dependents
|
||||
clean up and captured publishers cannot outlive their module. Direct object-delivery URLs remain
|
||||
opaque payload strings and are never re-signed per publication or subscriber. MQTT remains an
|
||||
external-control concern for a future Companion boundary, not an internal module bus. Display/kiosk
|
||||
hosting and its public/device principal are explicitly not part of this pass.
|
||||
|
||||
Reference in New Issue
Block a user