@c9up/aurora 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,6 +18,7 @@
18
18
  interface AuroraContainer {
19
19
  singleton(token: unknown, factory: () => unknown): void;
20
20
  resolve<T = unknown>(token: unknown): T;
21
+ has(token: unknown): boolean;
21
22
  }
22
23
  interface AuroraConfigStore {
23
24
  get<T = unknown>(key: string): T | undefined;
@@ -48,38 +48,23 @@ export default class AuroraProvider {
48
48
  setAurora(manager);
49
49
  }
50
50
  async start() {
51
- // Asset routes are registered in `start()` — after preloads —
52
- // so apps can swap aurora's pages root in a preload if they
53
- // wanted to. Non-Ream hosts (no `@c9up/ream/services/router`)
54
- // AND pre-`setRouter` boots (router proxy uninit) both
55
- // silent-return; ANY other error (slug collision, AuroraManager
56
- // crash, factory bug) propagates so real regressions surface
57
- // with a stack instead of "the asset routes just stopped
58
- // mounting".
59
- // Variable specifier so tsc does not statically resolve the optional
60
- // `@c9up/ream` peer at build time (keeps aurora agnostic /
61
- // standalone-buildable). Resolved to the host router only at runtime
62
- // when aurora actually runs inside Ream.
63
- const routerSpecifier = "@c9up/ream/services/router";
64
- let routerMod;
65
- try {
66
- routerMod = await import(routerSpecifier);
67
- }
68
- catch (err) {
69
- if (isModuleNotFound(err))
70
- return;
71
- throw err;
72
- }
51
+ // Asset routes are registered in `start()` — after preloads — so apps can
52
+ // swap aurora's pages root in a preload if they wanted to.
53
+ //
54
+ // Resolve the host router from the container, where Ream registers it as
55
+ // `'router'` (Ignitor). Reading it from the container — instead of
56
+ // importing `@c9up/ream/services/router` keeps aurora runtime-agnostic:
57
+ // a non-Ream host simply never registers `'router'`, so aurora silently
58
+ // skips its asset routes. The container yields the real Router instance
59
+ // (registered before any provider's `start()`), so route-registration
60
+ // failures (slug collision, AuroraManager crash) propagate with a stack
61
+ // instead of being misread as "the asset routes just stopped mounting".
62
+ if (!this.app.container.has("router"))
63
+ return;
64
+ const router = this.app.container.resolve("router");
73
65
  const manager = this.app.container.resolve(AuroraManager);
74
- try {
75
- routerMod.default.get("/_assets/aurora/*", adaptHandler(manager.auroraAssetsHandler()));
76
- routerMod.default.get("/_assets/pages/*", adaptHandler(manager.pageAssetsHandler()));
77
- }
78
- catch (err) {
79
- if (isRouterProxyUninit(err))
80
- return;
81
- throw err;
82
- }
66
+ router.get("/_assets/aurora/*", adaptHandler(manager.auroraAssetsHandler()));
67
+ router.get("/_assets/pages/*", adaptHandler(manager.pageAssetsHandler()));
83
68
  }
84
69
  async ready() { }
85
70
  async shutdown() { }
@@ -131,15 +116,3 @@ export default class AuroraProvider {
131
116
  function adaptHandler(handler) {
132
117
  return (ctx) => handler(ctx);
133
118
  }
134
- /** Node's ERR_MODULE_NOT_FOUND surfaces on an Error subclass with `code`. */
135
- function isModuleNotFound(err) {
136
- if (err === null || typeof err !== "object" || !("code" in err))
137
- return false;
138
- const { code } = err;
139
- return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
140
- }
141
- /** Ream's router proxy throws this exact string before Ignitor wires it. */
142
- function isRouterProxyUninit(err) {
143
- return (err instanceof Error &&
144
- err.message.includes("Router accessed before initialization"));
145
- }
package/dist/index.d.ts CHANGED
@@ -13,4 +13,10 @@ export { batch, effect, isSignal, memo, onCleanup, type ReadSignal, type Signal,
13
13
  export { type Disposer, render } from "./render.js";
14
14
  export { type AuroraHttpContext, type AuroraResponse, type AuroraRouteConfig, auroraRoute, } from "./route.js";
15
15
  export { renderToString } from "./ssr.js";
16
+ export { type LiveComponentDefinition, type LiveSession, mountLiveSession, type SlotPatch, } from "./live.js";
17
+ export { createLiveRegistry, type LiveRegistry, type LiveSessionHandle, } from "./liveRegistry.js";
18
+ export { connectPatches, type LiveStore, liveStore, type RelayBroadcaster, } from "./liveBroadcast.js";
19
+ export { createLiveRouter, type LiveMount, type LiveRouter, } from "./liveRouter.js";
20
+ export { buildLiveTransport, liveClient, type LiveClientOptions, type LiveClientTransport, type LiveHttpPoster, type RelaySubscribeClient, } from "./liveClient.js";
21
+ export { DEFAULT_LIVE_EVENT_PATH, type LiveHttpContext, type LiveHttpRouter, wireLiveEvents, type WireLiveEventsOptions, } from "./liveServer.js";
16
22
  export type { TemplateResult } from "./types.js";
package/dist/index.js CHANGED
@@ -9,3 +9,9 @@ export { batch, effect, isSignal, memo, onCleanup, signal, untrack, } from "./re
9
9
  export { render } from "./render.js";
10
10
  export { auroraRoute, } from "./route.js";
11
11
  export { renderToString } from "./ssr.js";
12
+ export { mountLiveSession, } from "./live.js";
13
+ export { createLiveRegistry, } from "./liveRegistry.js";
14
+ export { connectPatches, liveStore, } from "./liveBroadcast.js";
15
+ export { createLiveRouter, } from "./liveRouter.js";
16
+ export { buildLiveTransport, liveClient, } from "./liveClient.js";
17
+ export { DEFAULT_LIVE_EVENT_PATH, wireLiveEvents, } from "./liveServer.js";
package/dist/live.d.ts ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Live components — server-resident reactive UI. The state lives on the server
3
+ * as ordinary aurora signals; this module turns a fine-grained signal change
4
+ * into a PRECISE per-slot patch (`{slot, value}`) instead of re-rendering the
5
+ * whole component. That precision — each signal already knows which template
6
+ * slot it feeds — is the angle that beats HTML-diffing live-view libraries.
7
+ *
8
+ * This is the transport-agnostic CORE (Stage 1): mount a session, render its
9
+ * initial HTML, dispatch events that mutate signals, and drain / subscribe to
10
+ * the patches produced. Wiring patches over `@c9up/relay` (SSE down, POST up)
11
+ * and the thin client applier live in later stages.
12
+ *
13
+ * Node-free / isomorphic: uses only `signal`/`effect`/`renderToString`, so it
14
+ * sits in the main barrel alongside the rest of aurora's runtime.
15
+ */
16
+ import type { TemplateResult } from "./types.js";
17
+ /** A precise per-slot update: slot index (positional in the template) + value. */
18
+ export interface SlotPatch {
19
+ slot: number;
20
+ value: string;
21
+ }
22
+ /** A live component: its reactive view + named event handlers that mutate it. */
23
+ export interface LiveComponentDefinition {
24
+ /** The reactive view. Slots reading signals become live-patchable. */
25
+ view: TemplateResult;
26
+ /**
27
+ * Event handlers, by name. A client interaction (`@click="increment"`)
28
+ * dispatches one of these; it mutates the component's signals, which the
29
+ * patch tracker turns into a `{slot, value}` patch.
30
+ */
31
+ handlers?: Record<string, (payload?: unknown) => void>;
32
+ }
33
+ /** A mounted live component instance — one per connected client session. */
34
+ export interface LiveSession {
35
+ /** Initial server-side render (the first full-HTML response). */
36
+ renderToString(): string;
37
+ /** Run a named handler (mutates signals); emits a patch for the batch. */
38
+ dispatch(event: string, payload?: unknown): void;
39
+ /** Collect + clear the patches accumulated since the last drain (pull model). */
40
+ drainPatches(): SlotPatch[];
41
+ /**
42
+ * Subscribe to patches as they are produced (push model — what the relay
43
+ * transport hooks into). Use `onPatch` OR `drainPatches`, not both.
44
+ */
45
+ onPatch(listener: (patch: SlotPatch[]) => void): () => void;
46
+ /** Stop every effect — call on client disconnect to free the session. */
47
+ dispose(): void;
48
+ }
49
+ /**
50
+ * Mount a live session from a definition factory. Call once per connected
51
+ * client: the factory's signals become that session's private state. To SHARE
52
+ * state across sessions, close the factory over a signal created OUTSIDE it —
53
+ * every session then reads the same signal and patches on its change.
54
+ */
55
+ export declare function mountLiveSession(factory: () => LiveComponentDefinition): LiveSession;
package/dist/live.js ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Live components — server-resident reactive UI. The state lives on the server
3
+ * as ordinary aurora signals; this module turns a fine-grained signal change
4
+ * into a PRECISE per-slot patch (`{slot, value}`) instead of re-rendering the
5
+ * whole component. That precision — each signal already knows which template
6
+ * slot it feeds — is the angle that beats HTML-diffing live-view libraries.
7
+ *
8
+ * This is the transport-agnostic CORE (Stage 1): mount a session, render its
9
+ * initial HTML, dispatch events that mutate signals, and drain / subscribe to
10
+ * the patches produced. Wiring patches over `@c9up/relay` (SSE down, POST up)
11
+ * and the thin client applier live in later stages.
12
+ *
13
+ * Node-free / isomorphic: uses only `signal`/`effect`/`renderToString`, so it
14
+ * sits in the main barrel alongside the rest of aurora's runtime.
15
+ */
16
+ import { effect, isSignal } from "./reactive.js";
17
+ import { renderToString } from "./ssr.js";
18
+ /**
19
+ * Mount a live session from a definition factory. Call once per connected
20
+ * client: the factory's signals become that session's private state. To SHARE
21
+ * state across sessions, close the factory over a signal created OUTSIDE it —
22
+ * every session then reads the same signal and patches on its change.
23
+ */
24
+ export function mountLiveSession(factory) {
25
+ const { view, handlers = {} } = factory();
26
+ const listeners = new Set();
27
+ // slot → latest value. `pending` = current (un-flushed) batch; `buffer` =
28
+ // accumulated for pull consumers. Both keyed by slot so repeated writes in
29
+ // one batch collapse to the last value.
30
+ const pending = new Map();
31
+ const buffer = new Map();
32
+ let priming = true;
33
+ let flushScheduled = false;
34
+ const flush = () => {
35
+ flushScheduled = false;
36
+ if (pending.size === 0)
37
+ return;
38
+ const patch = [];
39
+ for (const [slot, value] of pending) {
40
+ patch.push({ slot, value });
41
+ buffer.set(slot, value);
42
+ }
43
+ pending.clear();
44
+ for (const listener of listeners)
45
+ listener(patch);
46
+ };
47
+ const scheduleFlush = () => {
48
+ if (flushScheduled)
49
+ return;
50
+ flushScheduled = true;
51
+ queueMicrotask(flush);
52
+ };
53
+ // One fine-grained effect per reactive slot. The priming run only
54
+ // subscribes; later runs (a signal changed) record the slot's new value.
55
+ const stops = view.values.map((value, slot) => {
56
+ if (!isSignal(value) && typeof value !== "function")
57
+ return () => { };
58
+ return effect(() => {
59
+ const next = String(value());
60
+ if (priming)
61
+ return;
62
+ pending.set(slot, next);
63
+ scheduleFlush();
64
+ });
65
+ });
66
+ priming = false;
67
+ return {
68
+ renderToString: () => renderToString(view),
69
+ dispatch(event, payload) {
70
+ const handler = handlers[event];
71
+ if (!handler)
72
+ return;
73
+ handler(payload);
74
+ flush(); // synchronous — one patch per dispatch (batches the handler's writes)
75
+ },
76
+ drainPatches() {
77
+ flush();
78
+ const patch = [];
79
+ for (const [slot, value] of buffer)
80
+ patch.push({ slot, value });
81
+ buffer.clear();
82
+ return patch;
83
+ },
84
+ onPatch(listener) {
85
+ listeners.add(listener);
86
+ return () => listeners.delete(listener);
87
+ },
88
+ dispose() {
89
+ for (const stop of stops)
90
+ stop();
91
+ listeners.clear();
92
+ pending.clear();
93
+ buffer.clear();
94
+ },
95
+ };
96
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Live broadcast (Stage 3) — pipe live-component patches onto a relay channel,
3
+ * and the SHARED-store primitive that makes live components multiplayer.
4
+ *
5
+ * aurora stays framework-agnostic: it DUCK-TYPES a {@link RelayBroadcaster}
6
+ * (just `broadcast(channel, data)`) and never imports `@c9up/relay`. The app
7
+ * passes its relay instance. Channel authorization (who may subscribe) is the
8
+ * relay's job — configure it with `relay.authorize(channel, …)`; aurora only
9
+ * pushes patches.
10
+ *
11
+ * Per-session use: `connectPatches(session, relay, "live/<id>")`.
12
+ * Shared/multiplayer use: `liveStore(factory, relay, "room/<id>")` — ONE
13
+ * server-side instance whose state is shared by every client on the channel;
14
+ * one mutation → one patch computed once → relay fans it out to all subscribers
15
+ * (O(1) compute, O(N) network).
16
+ */
17
+ import { type LiveComponentDefinition, type LiveSession } from "./live.js";
18
+ /**
19
+ * Minimal relay surface aurora needs. The real `@c9up/relay` `Relay` satisfies
20
+ * it (`broadcast(channel, data) → recipient count`); aurora never imports it.
21
+ */
22
+ export interface RelayBroadcaster {
23
+ broadcast(channel: string, data: unknown): number;
24
+ }
25
+ /**
26
+ * Pipe a session's patches onto a relay channel as they are produced. Returns
27
+ * an unsubscribe. Each patch becomes one `broadcast(channel, patch)`.
28
+ */
29
+ export declare function connectPatches(session: LiveSession, relay: RelayBroadcaster, channel: string): () => void;
30
+ /**
31
+ * A shared, broadcast-backed live store — the multiplayer primitive. One
32
+ * server-side instance; every client on `channel` renders its initial HTML and
33
+ * subscribes for patches. A `dispatch` mutates the shared signals ONCE; the
34
+ * resulting patch is broadcast to the whole channel.
35
+ */
36
+ export interface LiveStore {
37
+ /** The relay channel this store broadcasts on. */
38
+ readonly channel: string;
39
+ /** Current shared-state HTML — served to each client that joins. */
40
+ renderToString(): string;
41
+ /** Run a handler that mutates the shared state → one broadcast patch. */
42
+ dispatch(event: string, payload?: unknown): void;
43
+ /** Stop broadcasting + free the underlying session. */
44
+ dispose(): void;
45
+ }
46
+ /**
47
+ * Create a shared live store. Signals declared inside `factory` are the SHARED
48
+ * state (one instance, not per-client). Pair with `relay.authorize(channel, …)`
49
+ * to gate who may subscribe.
50
+ */
51
+ export declare function liveStore(factory: () => LiveComponentDefinition, relay: RelayBroadcaster, channel: string): LiveStore;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Live broadcast (Stage 3) — pipe live-component patches onto a relay channel,
3
+ * and the SHARED-store primitive that makes live components multiplayer.
4
+ *
5
+ * aurora stays framework-agnostic: it DUCK-TYPES a {@link RelayBroadcaster}
6
+ * (just `broadcast(channel, data)`) and never imports `@c9up/relay`. The app
7
+ * passes its relay instance. Channel authorization (who may subscribe) is the
8
+ * relay's job — configure it with `relay.authorize(channel, …)`; aurora only
9
+ * pushes patches.
10
+ *
11
+ * Per-session use: `connectPatches(session, relay, "live/<id>")`.
12
+ * Shared/multiplayer use: `liveStore(factory, relay, "room/<id>")` — ONE
13
+ * server-side instance whose state is shared by every client on the channel;
14
+ * one mutation → one patch computed once → relay fans it out to all subscribers
15
+ * (O(1) compute, O(N) network).
16
+ */
17
+ import { mountLiveSession, } from "./live.js";
18
+ /**
19
+ * Pipe a session's patches onto a relay channel as they are produced. Returns
20
+ * an unsubscribe. Each patch becomes one `broadcast(channel, patch)`.
21
+ */
22
+ export function connectPatches(session, relay, channel) {
23
+ return session.onPatch((patch) => {
24
+ relay.broadcast(channel, patch);
25
+ });
26
+ }
27
+ /**
28
+ * Create a shared live store. Signals declared inside `factory` are the SHARED
29
+ * state (one instance, not per-client). Pair with `relay.authorize(channel, …)`
30
+ * to gate who may subscribe.
31
+ */
32
+ export function liveStore(factory, relay, channel) {
33
+ const session = mountLiveSession(factory);
34
+ const off = connectPatches(session, relay, channel);
35
+ return {
36
+ channel,
37
+ renderToString: () => session.renderToString(),
38
+ dispatch: (event, payload) => session.dispatch(event, payload),
39
+ dispose: () => {
40
+ off();
41
+ session.dispose();
42
+ },
43
+ };
44
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Live client runtime (Stage 5) — the thin browser side of live components.
3
+ *
4
+ * It HYDRATES the server-rendered HTML with mirror signals (aurora's real
5
+ * `hydrate`), then:
6
+ * - applies inbound patches by SETTING the mirror signal at the patched slot
7
+ * → aurora's fine-grained binding updates the exact DOM node (no bespoke
8
+ * DOM patcher; the isomorphic renderer IS the applier);
9
+ * - forwards interactions declared with `data-live-click="<event>"` to the
10
+ * server via the injected transport.
11
+ *
12
+ * Transport-agnostic: `subscribe` (relay SSE) and `post` (HTTP up) are injected,
13
+ * so aurora never imports `@c9up/relay` or an HTTP client here. In an app, wire
14
+ * `subscribe` to `@c9up/aurora/relay`'s `relay().subscribe` and `post` to an
15
+ * `HttpClient`. Browser-only (uses the DOM) — part of the client barrel.
16
+ */
17
+ import type { SlotPatch } from "./live.js";
18
+ import type { TemplateResult } from "./types.js";
19
+ /** The transport the live client needs: a patch subscription + an event POST. */
20
+ export interface LiveClientTransport {
21
+ /** Subscribe to a channel's patches (relay SSE). Returns an unsubscribe. */
22
+ subscribe(channel: string, handler: (patch: SlotPatch[]) => void): () => void;
23
+ /** Send a client event to the server (HTTP POST up). */
24
+ post(id: string, event: string, payload?: unknown): void;
25
+ }
26
+ export interface LiveClientOptions {
27
+ /** The element holding the server-rendered HTML to adopt. */
28
+ container: Element;
29
+ /** The client view (same template as the server; its signals are mirrors). */
30
+ factory: () => TemplateResult;
31
+ /** Ids from the server's mount response. */
32
+ mount: {
33
+ id: string;
34
+ channel: string;
35
+ };
36
+ transport: LiveClientTransport;
37
+ }
38
+ /**
39
+ * Start the live client for one mounted component. Returns a disposer that
40
+ * unsubscribes, removes the event listener, and tears down the hydration.
41
+ *
42
+ * Patches set writable signal slots; derived slots recompute locally from the
43
+ * base signals they read (aurora re-evaluates them) — so the server's
44
+ * base-signal patch is enough. (Derived-only slots with no mirrored base are an
45
+ * étape-6 refinement.)
46
+ *
47
+ * Authoring rule (aurora hydration): a reactive text slot must be the SOLE
48
+ * content of its element — write `Count: <span>${count}</span>`, not
49
+ * `Count: ${count}`. SSR merges adjacent static+dynamic text into one node,
50
+ * which hydration cannot re-split; isolating the slot keeps adopt + patch exact.
51
+ */
52
+ export declare function liveClient(opts: LiveClientOptions): () => void;
53
+ /** The relay client slice the transport needs (`@c9up/aurora/relay` satisfies it). */
54
+ export interface RelaySubscribeClient {
55
+ subscribe<E>(channel: string, handler: (event: E) => void): () => void;
56
+ }
57
+ /** The HTTP client slice the transport needs (aurora's `HttpClient` satisfies it). */
58
+ export interface LiveHttpPoster {
59
+ post(url: string, body: unknown): unknown;
60
+ }
61
+ /**
62
+ * Build a {@link LiveClientTransport} from a relay client (SSE down) + an HTTP
63
+ * client (events up). `path` must match the server's `wireLiveEvents` route
64
+ * (default `/_live/event`). Keeps `liveClient` itself transport-agnostic.
65
+ *
66
+ * @example
67
+ * import { relay } from '@c9up/aurora/relay'
68
+ * import { HttpClient, buildLiveTransport, liveClient } from '@c9up/aurora'
69
+ * const transport = buildLiveTransport(relay(), new HttpClient())
70
+ * liveClient({ container, factory, mount, transport })
71
+ */
72
+ export declare function buildLiveTransport(relayClient: RelaySubscribeClient, http: LiveHttpPoster, options?: {
73
+ path?: string;
74
+ }): LiveClientTransport;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Live client runtime (Stage 5) — the thin browser side of live components.
3
+ *
4
+ * It HYDRATES the server-rendered HTML with mirror signals (aurora's real
5
+ * `hydrate`), then:
6
+ * - applies inbound patches by SETTING the mirror signal at the patched slot
7
+ * → aurora's fine-grained binding updates the exact DOM node (no bespoke
8
+ * DOM patcher; the isomorphic renderer IS the applier);
9
+ * - forwards interactions declared with `data-live-click="<event>"` to the
10
+ * server via the injected transport.
11
+ *
12
+ * Transport-agnostic: `subscribe` (relay SSE) and `post` (HTTP up) are injected,
13
+ * so aurora never imports `@c9up/relay` or an HTTP client here. In an app, wire
14
+ * `subscribe` to `@c9up/aurora/relay`'s `relay().subscribe` and `post` to an
15
+ * `HttpClient`. Browser-only (uses the DOM) — part of the client barrel.
16
+ */
17
+ import { hydrate } from "./hydrate.js";
18
+ import { isSignal } from "./reactive.js";
19
+ /**
20
+ * Start the live client for one mounted component. Returns a disposer that
21
+ * unsubscribes, removes the event listener, and tears down the hydration.
22
+ *
23
+ * Patches set writable signal slots; derived slots recompute locally from the
24
+ * base signals they read (aurora re-evaluates them) — so the server's
25
+ * base-signal patch is enough. (Derived-only slots with no mirrored base are an
26
+ * étape-6 refinement.)
27
+ *
28
+ * Authoring rule (aurora hydration): a reactive text slot must be the SOLE
29
+ * content of its element — write `Count: <span>${count}</span>`, not
30
+ * `Count: ${count}`. SSR merges adjacent static+dynamic text into one node,
31
+ * which hydration cannot re-split; isolating the slot keeps adopt + patch exact.
32
+ */
33
+ export function liveClient(opts) {
34
+ const view = opts.factory();
35
+ const disposeHydrate = hydrate(opts.container, () => view);
36
+ const off = opts.transport.subscribe(opts.mount.channel, (patch) => {
37
+ for (const { slot, value } of patch) {
38
+ const sig = view.values[slot];
39
+ if (isSignal(sig))
40
+ sig(value);
41
+ }
42
+ });
43
+ const onClick = (event) => {
44
+ const target = event.target;
45
+ if (!(target instanceof Element))
46
+ return;
47
+ const el = target.closest("[data-live-click]");
48
+ if (el)
49
+ opts.transport.post(opts.mount.id, el.getAttribute("data-live-click") ?? "");
50
+ };
51
+ opts.container.addEventListener("click", onClick);
52
+ return () => {
53
+ off();
54
+ opts.container.removeEventListener("click", onClick);
55
+ disposeHydrate();
56
+ };
57
+ }
58
+ /**
59
+ * Build a {@link LiveClientTransport} from a relay client (SSE down) + an HTTP
60
+ * client (events up). `path` must match the server's `wireLiveEvents` route
61
+ * (default `/_live/event`). Keeps `liveClient` itself transport-agnostic.
62
+ *
63
+ * @example
64
+ * import { relay } from '@c9up/aurora/relay'
65
+ * import { HttpClient, buildLiveTransport, liveClient } from '@c9up/aurora'
66
+ * const transport = buildLiveTransport(relay(), new HttpClient())
67
+ * liveClient({ container, factory, mount, transport })
68
+ */
69
+ export function buildLiveTransport(relayClient, http, options = {}) {
70
+ const path = options.path ?? "/_live/event";
71
+ return {
72
+ subscribe: (channel, handler) => relayClient.subscribe(channel, handler),
73
+ post: (id, event, payload) => {
74
+ void http.post(path, { id, event, payload });
75
+ },
76
+ };
77
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Live session registry (Stage 2) — the lifecycle layer for live components.
3
+ *
4
+ * Holds component DEFINITIONS by name (like the Pages registry), mounts a fresh
5
+ * {@link LiveSession} per connected client, tracks ownership so every session a
6
+ * client opened can be torn down at once on disconnect, and exposes lookup so
7
+ * an inbound event reaches the right session.
8
+ *
9
+ * The transport stage drives it: connect → `mount(name, uid)`; event →
10
+ * `get(id)?.dispatch(...)`; disconnect → `disposeOwner(uid)`. Transport-agnostic
11
+ * and node-free (only `mountLiveSession` + `crypto.randomUUID`, both isomorphic).
12
+ */
13
+ import { type LiveComponentDefinition, type LiveSession } from "./live.js";
14
+ /** A mounted instance: its id, the owner (e.g. relay uid), and the session. */
15
+ export interface LiveSessionHandle {
16
+ id: string;
17
+ ownerId: string;
18
+ session: LiveSession;
19
+ }
20
+ export interface LiveRegistry {
21
+ /** Register a live component definition under `name` (the "live class"). */
22
+ define(name: string, factory: () => LiveComponentDefinition): void;
23
+ /** True if `name` is registered. */
24
+ has(name: string): boolean;
25
+ /**
26
+ * Mount a fresh session of `name` owned by `ownerId`. Each call gets its own
27
+ * per-session signals. Throws if `name` is unknown — an unmountable component
28
+ * must fail loudly, never silently serve nothing.
29
+ */
30
+ mount(name: string, ownerId: string): LiveSessionHandle;
31
+ /** Look up a live session by instance id. */
32
+ get(id: string): LiveSession | undefined;
33
+ /** Dispose one session instance (frees its effects). */
34
+ dispose(id: string): void;
35
+ /** Dispose EVERY session a given owner opened — call on disconnect. */
36
+ disposeOwner(ownerId: string): void;
37
+ /** Dispose all sessions (shutdown). */
38
+ disposeAll(): void;
39
+ /** Number of live sessions currently mounted (diagnostics / tests). */
40
+ size(): number;
41
+ }
42
+ /** Create an isolated live-session registry (one per app / per relay instance). */
43
+ export declare function createLiveRegistry(): LiveRegistry;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Live session registry (Stage 2) — the lifecycle layer for live components.
3
+ *
4
+ * Holds component DEFINITIONS by name (like the Pages registry), mounts a fresh
5
+ * {@link LiveSession} per connected client, tracks ownership so every session a
6
+ * client opened can be torn down at once on disconnect, and exposes lookup so
7
+ * an inbound event reaches the right session.
8
+ *
9
+ * The transport stage drives it: connect → `mount(name, uid)`; event →
10
+ * `get(id)?.dispatch(...)`; disconnect → `disposeOwner(uid)`. Transport-agnostic
11
+ * and node-free (only `mountLiveSession` + `crypto.randomUUID`, both isomorphic).
12
+ */
13
+ import { mountLiveSession, } from "./live.js";
14
+ /** Create an isolated live-session registry (one per app / per relay instance). */
15
+ export function createLiveRegistry() {
16
+ const defs = new Map();
17
+ const sessions = new Map();
18
+ const byOwner = new Map();
19
+ const dispose = (id) => {
20
+ const handle = sessions.get(id);
21
+ if (!handle)
22
+ return;
23
+ handle.session.dispose();
24
+ sessions.delete(id);
25
+ const owned = byOwner.get(handle.ownerId);
26
+ if (owned) {
27
+ owned.delete(id);
28
+ if (owned.size === 0)
29
+ byOwner.delete(handle.ownerId);
30
+ }
31
+ };
32
+ return {
33
+ define(name, factory) {
34
+ defs.set(name, factory);
35
+ },
36
+ has(name) {
37
+ return defs.has(name);
38
+ },
39
+ mount(name, ownerId) {
40
+ const factory = defs.get(name);
41
+ if (!factory) {
42
+ throw new Error(`[aurora:live] unknown live component "${name}" — register it with registry.define("${name}", …) before mounting.`);
43
+ }
44
+ const id = crypto.randomUUID();
45
+ const handle = {
46
+ id,
47
+ ownerId,
48
+ session: mountLiveSession(factory),
49
+ };
50
+ sessions.set(id, handle);
51
+ const owned = byOwner.get(ownerId) ?? new Set();
52
+ owned.add(id);
53
+ byOwner.set(ownerId, owned);
54
+ return handle;
55
+ },
56
+ get(id) {
57
+ return sessions.get(id)?.session;
58
+ },
59
+ dispose,
60
+ disposeOwner(ownerId) {
61
+ const owned = byOwner.get(ownerId);
62
+ if (!owned)
63
+ return;
64
+ // Copy ids first — `dispose` mutates the same set as it goes.
65
+ for (const id of [...owned])
66
+ dispose(id);
67
+ byOwner.delete(ownerId);
68
+ },
69
+ disposeAll() {
70
+ for (const id of [...sessions.keys()])
71
+ dispose(id);
72
+ },
73
+ size() {
74
+ return sessions.size;
75
+ },
76
+ };
77
+ }