@c9up/aurora 0.1.22 → 0.1.23

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.
package/dist/live.d.ts CHANGED
@@ -1,60 +1,20 @@
1
1
  /**
2
- * Live componentsserver-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.
2
+ * `@c9up/aurora/live` barrelthe whole Live surface behind ONE subpath.
7
3
  *
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.
4
+ * The transport-agnostic core (`mountLiveSession` + its types) lives in
5
+ * `./liveSession.js`; the transport stack (broadcast / client / registry /
6
+ * router / server) builds on it. Re-exporting everything here keeps the niche
7
+ * surface behind a single subpath and keeps the main `.` barrel lean (it no
8
+ * longer eager-pulls these into every browser graph), consistent with how
9
+ * `./ssr`, `./relay`, `./rpc` and `./hydrate` are already subpath-gated.
12
10
  *
13
- * Node-free / isomorphic: uses only `signal`/`effect`/`renderToString`, so it
14
- * sits in the main barrel alongside the rest of aurora's runtime.
11
+ * The core lives in a SEPARATE module (not inline here) so the transport files
12
+ * import it directly from `./liveSession.js` rather than through this barrel
13
+ * which would otherwise form an import cycle (barrel → transport → barrel).
15
14
  */
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;
56
15
  export { connectPatches, type LiveStore, liveStore, type RelayBroadcaster, } from "./liveBroadcast.js";
57
16
  export { buildLiveTransport, type LiveClientOptions, type LiveClientTransport, type LiveHttpPoster, liveClient, type RelaySubscribeClient, } from "./liveClient.js";
58
17
  export { createLiveRegistry, type LiveRegistry, type LiveSessionHandle, } from "./liveRegistry.js";
59
18
  export { createLiveRouter, type LiveMount, type LiveRouter, } from "./liveRouter.js";
60
19
  export { DEFAULT_LIVE_EVENT_PATH, type LiveHttpContext, type LiveHttpRouter, type WireLiveEventsOptions, wireLiveEvents, } from "./liveServer.js";
20
+ export { type LiveComponentDefinition, type LiveSession, mountLiveSession, type SlotPatch, } from "./liveSession.js";
package/dist/live.js CHANGED
@@ -1,108 +1,20 @@
1
1
  /**
2
- * Live componentsserver-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.
2
+ * `@c9up/aurora/live` barrelthe whole Live surface behind ONE subpath.
7
3
  *
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.
4
+ * The transport-agnostic core (`mountLiveSession` + its types) lives in
5
+ * `./liveSession.js`; the transport stack (broadcast / client / registry /
6
+ * router / server) builds on it. Re-exporting everything here keeps the niche
7
+ * surface behind a single subpath and keeps the main `.` barrel lean (it no
8
+ * longer eager-pulls these into every browser graph), consistent with how
9
+ * `./ssr`, `./relay`, `./rpc` and `./hydrate` are already subpath-gated.
12
10
  *
13
- * Node-free / isomorphic: uses only `signal`/`effect`/`renderToString`, so it
14
- * sits in the main barrel alongside the rest of aurora's runtime.
11
+ * The core lives in a SEPARATE module (not inline here) so the transport files
12
+ * import it directly from `./liveSession.js` rather than through this barrel
13
+ * which would otherwise form an import cycle (barrel → transport → barrel).
15
14
  */
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
- }
97
- // ─── `@c9up/aurora/live` barrel ──────────────────────────────────────
98
- //
99
- // The Live stack (broadcast / client / registry / router / server) is
100
- // re-exported here so the whole niche surface lives behind ONE subpath,
101
- // `@c9up/aurora/live` — keeping the main `.` barrel lean (it no longer
102
- // eager-pulls these into every browser graph), consistent with how `./ssr`,
103
- // `./relay`, `./rpc` and `./hydrate` are already subpath-gated.
104
15
  export { connectPatches, liveStore, } from "./liveBroadcast.js";
105
16
  export { buildLiveTransport, liveClient, } from "./liveClient.js";
106
17
  export { createLiveRegistry, } from "./liveRegistry.js";
107
18
  export { createLiveRouter, } from "./liveRouter.js";
108
19
  export { DEFAULT_LIVE_EVENT_PATH, wireLiveEvents, } from "./liveServer.js";
20
+ export { mountLiveSession, } from "./liveSession.js";
@@ -14,7 +14,7 @@
14
14
  * one mutation → one patch computed once → relay fans it out to all subscribers
15
15
  * (O(1) compute, O(N) network).
16
16
  */
17
- import { type LiveComponentDefinition, type LiveSession } from "./live.js";
17
+ import { type LiveComponentDefinition, type LiveSession } from "./liveSession.js";
18
18
  /**
19
19
  * Minimal relay surface aurora needs. The real `@c9up/relay` `Relay` satisfies
20
20
  * it (`broadcast(channel, data) → recipient count`); aurora never imports it.
@@ -14,7 +14,7 @@
14
14
  * one mutation → one patch computed once → relay fans it out to all subscribers
15
15
  * (O(1) compute, O(N) network).
16
16
  */
17
- import { mountLiveSession, } from "./live.js";
17
+ import { mountLiveSession, } from "./liveSession.js";
18
18
  /**
19
19
  * Pipe a session's patches onto a relay channel as they are produced. Returns
20
20
  * an unsubscribe. Each patch becomes one `broadcast(channel, patch)`.
@@ -14,7 +14,7 @@
14
14
  * `subscribe` to `@c9up/aurora/relay`'s `relay().subscribe` and `post` to an
15
15
  * `HttpClient`. Browser-only (uses the DOM) — part of the client barrel.
16
16
  */
17
- import type { SlotPatch } from "./live.js";
17
+ import type { SlotPatch } from "./liveSession.js";
18
18
  import type { TemplateResult } from "./types.js";
19
19
  /** The transport the live client needs: a patch subscription + an event POST. */
20
20
  export interface LiveClientTransport {
@@ -10,7 +10,7 @@
10
10
  * `get(id)?.dispatch(...)`; disconnect → `disposeOwner(uid)`. Transport-agnostic
11
11
  * and node-free (only `mountLiveSession` + `crypto.randomUUID`, both isomorphic).
12
12
  */
13
- import { type LiveComponentDefinition, type LiveSession } from "./live.js";
13
+ import { type LiveComponentDefinition, type LiveSession } from "./liveSession.js";
14
14
  /** A mounted instance: its id, the owner (e.g. relay uid), and the session. */
15
15
  export interface LiveSessionHandle {
16
16
  id: string;
@@ -10,7 +10,7 @@
10
10
  * `get(id)?.dispatch(...)`; disconnect → `disposeOwner(uid)`. Transport-agnostic
11
11
  * and node-free (only `mountLiveSession` + `crypto.randomUUID`, both isomorphic).
12
12
  */
13
- import { mountLiveSession, } from "./live.js";
13
+ import { mountLiveSession, } from "./liveSession.js";
14
14
  /** Create an isolated live-session registry (one per app / per relay instance). */
15
15
  export function createLiveRegistry() {
16
16
  const defs = new Map();
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Live components — server-resident reactive UI CORE. The state lives on the
3
+ * server as ordinary aurora signals; this module turns a fine-grained signal
4
+ * change into a PRECISE per-slot patch (`{slot, value}`) instead of re-rendering
5
+ * the 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: mount a session, render its initial HTML,
9
+ * dispatch events that mutate signals, and drain / subscribe to the patches
10
+ * produced. The transport stack (broadcast / client / registry / router /
11
+ * server) builds ON this and re-exports through the `./live.js` barrel — kept in
12
+ * a separate module so those transport files import the core WITHOUT forming an
13
+ * import cycle back through the barrel.
14
+ *
15
+ * Node-free / isomorphic: uses only `signal`/`effect`/`renderToString`.
16
+ */
17
+ import type { TemplateResult } from "./types.js";
18
+ /** A precise per-slot update: slot index (positional in the template) + value. */
19
+ export interface SlotPatch {
20
+ slot: number;
21
+ value: string;
22
+ }
23
+ /** A live component: its reactive view + named event handlers that mutate it. */
24
+ export interface LiveComponentDefinition {
25
+ /** The reactive view. Slots reading signals become live-patchable. */
26
+ view: TemplateResult;
27
+ /**
28
+ * Event handlers, by name. A client interaction (`@click="increment"`)
29
+ * dispatches one of these; it mutates the component's signals, which the
30
+ * patch tracker turns into a `{slot, value}` patch.
31
+ */
32
+ handlers?: Record<string, (payload?: unknown) => void>;
33
+ }
34
+ /** A mounted live component instance — one per connected client session. */
35
+ export interface LiveSession {
36
+ /** Initial server-side render (the first full-HTML response). */
37
+ renderToString(): string;
38
+ /** Run a named handler (mutates signals); emits a patch for the batch. */
39
+ dispatch(event: string, payload?: unknown): void;
40
+ /** Collect + clear the patches accumulated since the last drain (pull model). */
41
+ drainPatches(): SlotPatch[];
42
+ /**
43
+ * Subscribe to patches as they are produced (push model — what the relay
44
+ * transport hooks into). Use `onPatch` OR `drainPatches`, not both.
45
+ */
46
+ onPatch(listener: (patch: SlotPatch[]) => void): () => void;
47
+ /** Stop every effect — call on client disconnect to free the session. */
48
+ dispose(): void;
49
+ }
50
+ /**
51
+ * Mount a live session from a definition factory. Call once per connected
52
+ * client: the factory's signals become that session's private state. To SHARE
53
+ * state across sessions, close the factory over a signal created OUTSIDE it —
54
+ * every session then reads the same signal and patches on its change.
55
+ */
56
+ export declare function mountLiveSession(factory: () => LiveComponentDefinition): LiveSession;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Live components — server-resident reactive UI CORE. The state lives on the
3
+ * server as ordinary aurora signals; this module turns a fine-grained signal
4
+ * change into a PRECISE per-slot patch (`{slot, value}`) instead of re-rendering
5
+ * the 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: mount a session, render its initial HTML,
9
+ * dispatch events that mutate signals, and drain / subscribe to the patches
10
+ * produced. The transport stack (broadcast / client / registry / router /
11
+ * server) builds ON this and re-exports through the `./live.js` barrel — kept in
12
+ * a separate module so those transport files import the core WITHOUT forming an
13
+ * import cycle back through the barrel.
14
+ *
15
+ * Node-free / isomorphic: uses only `signal`/`effect`/`renderToString`.
16
+ */
17
+ import { effect, isSignal } from "./reactive.js";
18
+ import { renderToString } from "./ssr.js";
19
+ /**
20
+ * Mount a live session from a definition factory. Call once per connected
21
+ * client: the factory's signals become that session's private state. To SHARE
22
+ * state across sessions, close the factory over a signal created OUTSIDE it —
23
+ * every session then reads the same signal and patches on its change.
24
+ */
25
+ export function mountLiveSession(factory) {
26
+ const { view, handlers = {} } = factory();
27
+ const listeners = new Set();
28
+ // slot → latest value. `pending` = current (un-flushed) batch; `buffer` =
29
+ // accumulated for pull consumers. Both keyed by slot so repeated writes in
30
+ // one batch collapse to the last value.
31
+ const pending = new Map();
32
+ const buffer = new Map();
33
+ let priming = true;
34
+ let flushScheduled = false;
35
+ const flush = () => {
36
+ flushScheduled = false;
37
+ if (pending.size === 0)
38
+ return;
39
+ const patch = [];
40
+ for (const [slot, value] of pending) {
41
+ patch.push({ slot, value });
42
+ buffer.set(slot, value);
43
+ }
44
+ pending.clear();
45
+ for (const listener of listeners)
46
+ listener(patch);
47
+ };
48
+ const scheduleFlush = () => {
49
+ if (flushScheduled)
50
+ return;
51
+ flushScheduled = true;
52
+ queueMicrotask(flush);
53
+ };
54
+ // One fine-grained effect per reactive slot. The priming run only
55
+ // subscribes; later runs (a signal changed) record the slot's new value.
56
+ const stops = view.values.map((value, slot) => {
57
+ if (!isSignal(value) && typeof value !== "function")
58
+ return () => { };
59
+ return effect(() => {
60
+ const next = String(value());
61
+ if (priming)
62
+ return;
63
+ pending.set(slot, next);
64
+ scheduleFlush();
65
+ });
66
+ });
67
+ priming = false;
68
+ return {
69
+ renderToString: () => renderToString(view),
70
+ dispatch(event, payload) {
71
+ const handler = handlers[event];
72
+ if (!handler)
73
+ return;
74
+ handler(payload);
75
+ flush(); // synchronous — one patch per dispatch (batches the handler's writes)
76
+ },
77
+ drainPatches() {
78
+ flush();
79
+ const patch = [];
80
+ for (const [slot, value] of buffer)
81
+ patch.push({ slot, value });
82
+ buffer.clear();
83
+ return patch;
84
+ },
85
+ onPatch(listener) {
86
+ listeners.add(listener);
87
+ return () => listeners.delete(listener);
88
+ },
89
+ dispose() {
90
+ for (const stop of stops)
91
+ stop();
92
+ listeners.clear();
93
+ pending.clear();
94
+ buffer.clear();
95
+ },
96
+ };
97
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c9up/aurora",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "description": "Aurora — reactive UI runtime for the Ream framework. Tagged-template DOM, signal-based state, isomorphic SSR + hydration, zero build step.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/live.ts CHANGED
@@ -1,143 +1,18 @@
1
1
  /**
2
- * Live componentsserver-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.
2
+ * `@c9up/aurora/live` barrelthe whole Live surface behind ONE subpath.
7
3
  *
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.
4
+ * The transport-agnostic core (`mountLiveSession` + its types) lives in
5
+ * `./liveSession.js`; the transport stack (broadcast / client / registry /
6
+ * router / server) builds on it. Re-exporting everything here keeps the niche
7
+ * surface behind a single subpath and keeps the main `.` barrel lean (it no
8
+ * longer eager-pulls these into every browser graph), consistent with how
9
+ * `./ssr`, `./relay`, `./rpc` and `./hydrate` are already subpath-gated.
12
10
  *
13
- * Node-free / isomorphic: uses only `signal`/`effect`/`renderToString`, so it
14
- * sits in the main barrel alongside the rest of aurora's runtime.
11
+ * The core lives in a SEPARATE module (not inline here) so the transport files
12
+ * import it directly from `./liveSession.js` rather than through this barrel
13
+ * which would otherwise form an import cycle (barrel → transport → barrel).
15
14
  */
16
15
 
17
- import { effect, isSignal } from "./reactive.js";
18
- import { renderToString } from "./ssr.js";
19
- import type { TemplateResult } from "./types.js";
20
-
21
- /** A precise per-slot update: slot index (positional in the template) + value. */
22
- export interface SlotPatch {
23
- slot: number;
24
- value: string;
25
- }
26
-
27
- /** A live component: its reactive view + named event handlers that mutate it. */
28
- export interface LiveComponentDefinition {
29
- /** The reactive view. Slots reading signals become live-patchable. */
30
- view: TemplateResult;
31
- /**
32
- * Event handlers, by name. A client interaction (`@click="increment"`)
33
- * dispatches one of these; it mutates the component's signals, which the
34
- * patch tracker turns into a `{slot, value}` patch.
35
- */
36
- handlers?: Record<string, (payload?: unknown) => void>;
37
- }
38
-
39
- /** A mounted live component instance — one per connected client session. */
40
- export interface LiveSession {
41
- /** Initial server-side render (the first full-HTML response). */
42
- renderToString(): string;
43
- /** Run a named handler (mutates signals); emits a patch for the batch. */
44
- dispatch(event: string, payload?: unknown): void;
45
- /** Collect + clear the patches accumulated since the last drain (pull model). */
46
- drainPatches(): SlotPatch[];
47
- /**
48
- * Subscribe to patches as they are produced (push model — what the relay
49
- * transport hooks into). Use `onPatch` OR `drainPatches`, not both.
50
- */
51
- onPatch(listener: (patch: SlotPatch[]) => void): () => void;
52
- /** Stop every effect — call on client disconnect to free the session. */
53
- dispose(): void;
54
- }
55
-
56
- /**
57
- * Mount a live session from a definition factory. Call once per connected
58
- * client: the factory's signals become that session's private state. To SHARE
59
- * state across sessions, close the factory over a signal created OUTSIDE it —
60
- * every session then reads the same signal and patches on its change.
61
- */
62
- export function mountLiveSession(
63
- factory: () => LiveComponentDefinition,
64
- ): LiveSession {
65
- const { view, handlers = {} } = factory();
66
- const listeners = new Set<(patch: SlotPatch[]) => void>();
67
- // slot → latest value. `pending` = current (un-flushed) batch; `buffer` =
68
- // accumulated for pull consumers. Both keyed by slot so repeated writes in
69
- // one batch collapse to the last value.
70
- const pending = new Map<number, string>();
71
- const buffer = new Map<number, string>();
72
- let priming = true;
73
- let flushScheduled = false;
74
-
75
- const flush = (): void => {
76
- flushScheduled = false;
77
- if (pending.size === 0) return;
78
- const patch: SlotPatch[] = [];
79
- for (const [slot, value] of pending) {
80
- patch.push({ slot, value });
81
- buffer.set(slot, value);
82
- }
83
- pending.clear();
84
- for (const listener of listeners) listener(patch);
85
- };
86
-
87
- const scheduleFlush = (): void => {
88
- if (flushScheduled) return;
89
- flushScheduled = true;
90
- queueMicrotask(flush);
91
- };
92
-
93
- // One fine-grained effect per reactive slot. The priming run only
94
- // subscribes; later runs (a signal changed) record the slot's new value.
95
- const stops = view.values.map((value, slot) => {
96
- if (!isSignal(value) && typeof value !== "function") return () => {};
97
- return effect(() => {
98
- const next = String((value as () => unknown)());
99
- if (priming) return;
100
- pending.set(slot, next);
101
- scheduleFlush();
102
- });
103
- });
104
- priming = false;
105
-
106
- return {
107
- renderToString: () => renderToString(view),
108
- dispatch(event, payload) {
109
- const handler = handlers[event];
110
- if (!handler) return;
111
- handler(payload);
112
- flush(); // synchronous — one patch per dispatch (batches the handler's writes)
113
- },
114
- drainPatches() {
115
- flush();
116
- const patch: SlotPatch[] = [];
117
- for (const [slot, value] of buffer) patch.push({ slot, value });
118
- buffer.clear();
119
- return patch;
120
- },
121
- onPatch(listener) {
122
- listeners.add(listener);
123
- return () => listeners.delete(listener);
124
- },
125
- dispose() {
126
- for (const stop of stops) stop();
127
- listeners.clear();
128
- pending.clear();
129
- buffer.clear();
130
- },
131
- };
132
- }
133
-
134
- // ─── `@c9up/aurora/live` barrel ──────────────────────────────────────
135
- //
136
- // The Live stack (broadcast / client / registry / router / server) is
137
- // re-exported here so the whole niche surface lives behind ONE subpath,
138
- // `@c9up/aurora/live` — keeping the main `.` barrel lean (it no longer
139
- // eager-pulls these into every browser graph), consistent with how `./ssr`,
140
- // `./relay`, `./rpc` and `./hydrate` are already subpath-gated.
141
16
  export {
142
17
  connectPatches,
143
18
  type LiveStore,
@@ -169,3 +44,9 @@ export {
169
44
  type WireLiveEventsOptions,
170
45
  wireLiveEvents,
171
46
  } from "./liveServer.js";
47
+ export {
48
+ type LiveComponentDefinition,
49
+ type LiveSession,
50
+ mountLiveSession,
51
+ type SlotPatch,
52
+ } from "./liveSession.js";
@@ -20,7 +20,7 @@ import {
20
20
  type LiveSession,
21
21
  mountLiveSession,
22
22
  type SlotPatch,
23
- } from "./live.js";
23
+ } from "./liveSession.js";
24
24
 
25
25
  /**
26
26
  * Minimal relay surface aurora needs. The real `@c9up/relay` `Relay` satisfies
package/src/liveClient.ts CHANGED
@@ -16,8 +16,8 @@
16
16
  */
17
17
 
18
18
  import { hydrate } from "./hydrate.js";
19
+ import type { SlotPatch } from "./liveSession.js";
19
20
  import { isSignal } from "./reactive.js";
20
- import type { SlotPatch } from "./live.js";
21
21
  import type { TemplateResult } from "./types.js";
22
22
 
23
23
  /** The transport the live client needs: a patch subscription + an event POST. */
@@ -67,7 +67,11 @@ export function liveClient(opts: LiveClientOptions): () => void {
67
67
  const target = event.target;
68
68
  if (!(target instanceof Element)) return;
69
69
  const el = target.closest("[data-live-click]");
70
- if (el) opts.transport.post(opts.mount.id, el.getAttribute("data-live-click") ?? "");
70
+ if (el)
71
+ opts.transport.post(
72
+ opts.mount.id,
73
+ el.getAttribute("data-live-click") ?? "",
74
+ );
71
75
  };
72
76
  opts.container.addEventListener("click", onClick);
73
77
 
@@ -15,7 +15,7 @@ import {
15
15
  type LiveComponentDefinition,
16
16
  type LiveSession,
17
17
  mountLiveSession,
18
- } from "./live.js";
18
+ } from "./liveSession.js";
19
19
 
20
20
  /** A mounted instance: its id, the owner (e.g. relay uid), and the session. */
21
21
  export interface LiveSessionHandle {
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Live components — server-resident reactive UI CORE. The state lives on the
3
+ * server as ordinary aurora signals; this module turns a fine-grained signal
4
+ * change into a PRECISE per-slot patch (`{slot, value}`) instead of re-rendering
5
+ * the 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: mount a session, render its initial HTML,
9
+ * dispatch events that mutate signals, and drain / subscribe to the patches
10
+ * produced. The transport stack (broadcast / client / registry / router /
11
+ * server) builds ON this and re-exports through the `./live.js` barrel — kept in
12
+ * a separate module so those transport files import the core WITHOUT forming an
13
+ * import cycle back through the barrel.
14
+ *
15
+ * Node-free / isomorphic: uses only `signal`/`effect`/`renderToString`.
16
+ */
17
+
18
+ import { effect, isSignal } from "./reactive.js";
19
+ import { renderToString } from "./ssr.js";
20
+ import type { TemplateResult } from "./types.js";
21
+
22
+ /** A precise per-slot update: slot index (positional in the template) + value. */
23
+ export interface SlotPatch {
24
+ slot: number;
25
+ value: string;
26
+ }
27
+
28
+ /** A live component: its reactive view + named event handlers that mutate it. */
29
+ export interface LiveComponentDefinition {
30
+ /** The reactive view. Slots reading signals become live-patchable. */
31
+ view: TemplateResult;
32
+ /**
33
+ * Event handlers, by name. A client interaction (`@click="increment"`)
34
+ * dispatches one of these; it mutates the component's signals, which the
35
+ * patch tracker turns into a `{slot, value}` patch.
36
+ */
37
+ handlers?: Record<string, (payload?: unknown) => void>;
38
+ }
39
+
40
+ /** A mounted live component instance — one per connected client session. */
41
+ export interface LiveSession {
42
+ /** Initial server-side render (the first full-HTML response). */
43
+ renderToString(): string;
44
+ /** Run a named handler (mutates signals); emits a patch for the batch. */
45
+ dispatch(event: string, payload?: unknown): void;
46
+ /** Collect + clear the patches accumulated since the last drain (pull model). */
47
+ drainPatches(): SlotPatch[];
48
+ /**
49
+ * Subscribe to patches as they are produced (push model — what the relay
50
+ * transport hooks into). Use `onPatch` OR `drainPatches`, not both.
51
+ */
52
+ onPatch(listener: (patch: SlotPatch[]) => void): () => void;
53
+ /** Stop every effect — call on client disconnect to free the session. */
54
+ dispose(): void;
55
+ }
56
+
57
+ /**
58
+ * Mount a live session from a definition factory. Call once per connected
59
+ * client: the factory's signals become that session's private state. To SHARE
60
+ * state across sessions, close the factory over a signal created OUTSIDE it —
61
+ * every session then reads the same signal and patches on its change.
62
+ */
63
+ export function mountLiveSession(
64
+ factory: () => LiveComponentDefinition,
65
+ ): LiveSession {
66
+ const { view, handlers = {} } = factory();
67
+ const listeners = new Set<(patch: SlotPatch[]) => void>();
68
+ // slot → latest value. `pending` = current (un-flushed) batch; `buffer` =
69
+ // accumulated for pull consumers. Both keyed by slot so repeated writes in
70
+ // one batch collapse to the last value.
71
+ const pending = new Map<number, string>();
72
+ const buffer = new Map<number, string>();
73
+ let priming = true;
74
+ let flushScheduled = false;
75
+
76
+ const flush = (): void => {
77
+ flushScheduled = false;
78
+ if (pending.size === 0) return;
79
+ const patch: SlotPatch[] = [];
80
+ for (const [slot, value] of pending) {
81
+ patch.push({ slot, value });
82
+ buffer.set(slot, value);
83
+ }
84
+ pending.clear();
85
+ for (const listener of listeners) listener(patch);
86
+ };
87
+
88
+ const scheduleFlush = (): void => {
89
+ if (flushScheduled) return;
90
+ flushScheduled = true;
91
+ queueMicrotask(flush);
92
+ };
93
+
94
+ // One fine-grained effect per reactive slot. The priming run only
95
+ // subscribes; later runs (a signal changed) record the slot's new value.
96
+ const stops = view.values.map((value, slot) => {
97
+ if (!isSignal(value) && typeof value !== "function") return () => {};
98
+ return effect(() => {
99
+ const next = String((value as () => unknown)());
100
+ if (priming) return;
101
+ pending.set(slot, next);
102
+ scheduleFlush();
103
+ });
104
+ });
105
+ priming = false;
106
+
107
+ return {
108
+ renderToString: () => renderToString(view),
109
+ dispatch(event, payload) {
110
+ const handler = handlers[event];
111
+ if (!handler) return;
112
+ handler(payload);
113
+ flush(); // synchronous — one patch per dispatch (batches the handler's writes)
114
+ },
115
+ drainPatches() {
116
+ flush();
117
+ const patch: SlotPatch[] = [];
118
+ for (const [slot, value] of buffer) patch.push({ slot, value });
119
+ buffer.clear();
120
+ return patch;
121
+ },
122
+ onPatch(listener) {
123
+ listeners.add(listener);
124
+ return () => listeners.delete(listener);
125
+ },
126
+ dispose() {
127
+ for (const stop of stops) stop();
128
+ listeners.clear();
129
+ pending.clear();
130
+ buffer.clear();
131
+ },
132
+ };
133
+ }