@c9up/aurora 0.1.3

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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +36 -0
  3. package/dist/AuroraManager.d.ts +44 -0
  4. package/dist/AuroraManager.js +47 -0
  5. package/dist/AuroraProvider.d.ts +52 -0
  6. package/dist/AuroraProvider.js +145 -0
  7. package/dist/Pages.d.ts +78 -0
  8. package/dist/Pages.js +116 -0
  9. package/dist/component.d.ts +55 -0
  10. package/dist/component.js +97 -0
  11. package/dist/html.d.ts +30 -0
  12. package/dist/html.js +246 -0
  13. package/dist/hydrate.d.ts +29 -0
  14. package/dist/hydrate.js +379 -0
  15. package/dist/index.d.ts +12 -0
  16. package/dist/index.js +12 -0
  17. package/dist/reactive.d.ts +83 -0
  18. package/dist/reactive.js +217 -0
  19. package/dist/relay.d.ts +43 -0
  20. package/dist/relay.js +144 -0
  21. package/dist/render.d.ts +25 -0
  22. package/dist/render.js +283 -0
  23. package/dist/route.d.ts +64 -0
  24. package/dist/route.js +49 -0
  25. package/dist/server/renderPage.d.ts +62 -0
  26. package/dist/server/renderPage.js +83 -0
  27. package/dist/server/serveAssets.d.ts +43 -0
  28. package/dist/server/serveAssets.js +89 -0
  29. package/dist/services/main.d.ts +18 -0
  30. package/dist/services/main.js +31 -0
  31. package/dist/ssr.d.ts +22 -0
  32. package/dist/ssr.js +179 -0
  33. package/dist/types.d.ts +78 -0
  34. package/dist/types.js +15 -0
  35. package/package.json +69 -0
  36. package/src/AuroraManager.ts +76 -0
  37. package/src/AuroraProvider.ts +187 -0
  38. package/src/Pages.ts +164 -0
  39. package/src/component.ts +138 -0
  40. package/src/html.ts +296 -0
  41. package/src/hydrate.ts +518 -0
  42. package/src/index.ts +43 -0
  43. package/src/reactive.ts +265 -0
  44. package/src/relay.ts +171 -0
  45. package/src/render.ts +378 -0
  46. package/src/route.ts +96 -0
  47. package/src/server/renderPage.ts +135 -0
  48. package/src/server/serveAssets.ts +135 -0
  49. package/src/services/main.ts +40 -0
  50. package/src/ssr.ts +179 -0
  51. package/src/types.ts +97 -0
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Reactive core — signals + effects with auto-tracking.
3
+ *
4
+ * No proxies, no VDOM. A `signal<T>()` is a single read/write function that
5
+ * registers itself in the currently-running observer's dependency set when
6
+ * read, and notifies every dependent observer when written.
7
+ *
8
+ * Effects run their callback once eagerly, capture the signals they read,
9
+ * and re-run whenever any of those signals fires. Effects can return a
10
+ * cleanup function that runs before the next re-execution and at disposal.
11
+ */
12
+ /**
13
+ * Brand symbol so consumers can distinguish a signal from a plain function
14
+ * without instanceof. Exposed only via the `isSignal` guard — never call
15
+ * sites need to import this directly.
16
+ */
17
+ export const SIGNAL_BRAND = Symbol.for("aurora:signal");
18
+ // `undefined` entries are untrack() sentinels — a read while one is on
19
+ // top sees "no active observer" and registers no dependency.
20
+ const observerStack = [];
21
+ let batchDepth = 0;
22
+ const pendingNotifications = new Set();
23
+ function activeObserver() {
24
+ return observerStack[observerStack.length - 1];
25
+ }
26
+ /**
27
+ * Create a writable signal seeded with `initial`. Reads register the
28
+ * current observer; writes notify every observer that previously read.
29
+ *
30
+ * Optional `{ equals }` swaps the default `Object.is` check — return
31
+ * `true` to skip notifying observers (the new value is "the same").
32
+ */
33
+ export function signal(initial, options) {
34
+ const node = {
35
+ value: initial,
36
+ observers: new Set(),
37
+ equals: options?.equals ?? Object.is,
38
+ };
39
+ function accessor(...args) {
40
+ if (args.length === 0) {
41
+ const obs = activeObserver();
42
+ if (obs) {
43
+ node.observers.add(obs);
44
+ obs.dependencies.add(node);
45
+ }
46
+ return node.value;
47
+ }
48
+ const arg = args[0];
49
+ const next = typeof arg === "function"
50
+ ? arg(node.value)
51
+ : arg;
52
+ if (node.equals(node.value, next))
53
+ return;
54
+ node.value = next;
55
+ // Snapshot observers before iteration — an effect's run() may
56
+ // dispose itself (or peers) and mutate the live Set during the
57
+ // loop, which would skip notifications under for…of semantics.
58
+ const toNotify = [...node.observers];
59
+ if (batchDepth > 0) {
60
+ for (const eff of toNotify)
61
+ pendingNotifications.add(eff);
62
+ return;
63
+ }
64
+ for (const eff of toNotify) {
65
+ if (!eff.disposed)
66
+ eff.run();
67
+ }
68
+ }
69
+ accessor[SIGNAL_BRAND] = true;
70
+ signalNodes.set(accessor, node);
71
+ return accessor;
72
+ }
73
+ /**
74
+ * Test-only registry mapping a signal accessor to its backing node, so
75
+ * `observerCount` can assert the observer Set doesn't leak. A WeakMap
76
+ * keeps it off the public accessor surface and never retains a disposed
77
+ * signal.
78
+ */
79
+ const signalNodes = new WeakMap();
80
+ /**
81
+ * @internal Observer-count test seam for the untrack-leak invariant: a
82
+ * read inside `untrack()` must NOT add an entry to a signal's observer
83
+ * Set. Returns -1 for a value that isn't a tracked signal.
84
+ */
85
+ export function observerCount(sig) {
86
+ const node = signalNodes.get(sig);
87
+ return node ? node.observers.size : -1;
88
+ }
89
+ /** Runtime guard — distinguishes a signal accessor from any other callable. */
90
+ export function isSignal(value) {
91
+ return (typeof value === "function" &&
92
+ value[SIGNAL_BRAND] === true);
93
+ }
94
+ /**
95
+ * Run `fn` immediately and every time a signal it reads changes. Returns a
96
+ * dispose function — call it to stop the effect and run any pending
97
+ * cleanup.
98
+ *
99
+ * Inside `fn`, return another function to register cleanup that runs
100
+ * before the next execution AND at disposal. Multiple cleanups can also
101
+ * be registered via `onCleanup()`.
102
+ */
103
+ export function effect(fn) {
104
+ const eff = {
105
+ dependencies: new Set(),
106
+ cleanups: [],
107
+ disposed: false,
108
+ run() {
109
+ if (this.disposed)
110
+ return;
111
+ runCleanups(this);
112
+ detach(this);
113
+ observerStack.push(this);
114
+ try {
115
+ const teardown = fn();
116
+ if (typeof teardown === "function")
117
+ this.cleanups.push(teardown);
118
+ }
119
+ finally {
120
+ observerStack.pop();
121
+ }
122
+ },
123
+ dispose() {
124
+ if (this.disposed)
125
+ return;
126
+ this.disposed = true;
127
+ runCleanups(this);
128
+ detach(this);
129
+ },
130
+ };
131
+ eff.run();
132
+ return () => eff.dispose();
133
+ }
134
+ /**
135
+ * Register a cleanup callback against the currently-running effect.
136
+ * No-op when called outside an effect — same contract as Solid's
137
+ * `onCleanup`, more permissive than React's hook-only access.
138
+ */
139
+ export function onCleanup(fn) {
140
+ const obs = activeObserver();
141
+ if (obs)
142
+ obs.cleanups.push(fn);
143
+ }
144
+ /**
145
+ * Defer notifications until `fn` returns. Multiple writes to the same
146
+ * signal coalesce into a single observer re-run, and writes across
147
+ * signals re-run each affected observer at most once.
148
+ */
149
+ export function batch(fn) {
150
+ batchDepth++;
151
+ try {
152
+ return fn();
153
+ }
154
+ finally {
155
+ batchDepth--;
156
+ if (batchDepth === 0) {
157
+ const toRun = [...pendingNotifications];
158
+ pendingNotifications.clear();
159
+ for (const eff of toRun) {
160
+ if (!eff.disposed)
161
+ eff.run();
162
+ }
163
+ }
164
+ }
165
+ }
166
+ /**
167
+ * Read signals inside `fn` without registering them as dependencies of
168
+ * the current observer. Useful when an effect needs the current value of
169
+ * a signal but should not re-run when it changes.
170
+ */
171
+ export function untrack(fn) {
172
+ // Push an `undefined` sentinel rather than a dummy Effect. A dummy
173
+ // gets `add()`-ed into every signal's `observers` Set on read and is
174
+ // never detached, leaking dead entries that grow each write. With
175
+ // `undefined` on top, `activeObserver()` returns undefined and reads
176
+ // register nothing — the actual "untracked" semantics.
177
+ observerStack.push(undefined);
178
+ try {
179
+ return fn();
180
+ }
181
+ finally {
182
+ observerStack.pop();
183
+ }
184
+ }
185
+ /**
186
+ * Derived read-only signal — `fn` re-runs when any signal it reads
187
+ * changes, and the latest return value is cached + handed out via the
188
+ * returned accessor. Cleanups inside `fn` (via `onCleanup`) run on every
189
+ * recomputation.
190
+ */
191
+ export function memo(fn) {
192
+ const internal = signal(undefined);
193
+ effect(() => {
194
+ internal(fn());
195
+ });
196
+ const reader = (() => internal());
197
+ reader[SIGNAL_BRAND] = true;
198
+ return reader;
199
+ }
200
+ function runCleanups(eff) {
201
+ if (eff.cleanups.length === 0)
202
+ return;
203
+ const queued = eff.cleanups.splice(0, eff.cleanups.length);
204
+ for (const cleanup of queued) {
205
+ try {
206
+ cleanup();
207
+ }
208
+ catch {
209
+ /* swallow — cleanup errors must not block sibling cleanups */
210
+ }
211
+ }
212
+ }
213
+ function detach(eff) {
214
+ for (const dep of eff.dependencies)
215
+ dep.observers.delete(eff);
216
+ eff.dependencies.clear();
217
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Browser-side relay helper — hides the EventSource + POST handshake
3
+ * Ream's `@c9up/relay` package expects.
4
+ *
5
+ * import { relay } from '@c9up/aurora/relay'
6
+ *
7
+ * relay().subscribe(`project/${id}`, (ev) => {
8
+ * console.log('received', ev)
9
+ * })
10
+ *
11
+ * One EventSource per page (a singleton inside this module). All
12
+ * subscribe calls fan out to its uid. The connection re-opens after
13
+ * the browser auto-reconnect; subscriptions are re-applied.
14
+ *
15
+ * This module is browser-only. It's shipped via aurora's pre-built
16
+ * `dist/` and imported through the same importmap that maps
17
+ * `@c9up/aurora`. Node-side code that pulls it will trip on
18
+ * `EventSource` being undefined.
19
+ */
20
+ export interface RelayClient {
21
+ subscribe<E>(channel: string, handler: (event: E) => void): () => void;
22
+ close(): void;
23
+ }
24
+ export interface RelayOptions {
25
+ /** SSE endpoint. Defaults to `/__relay/events`. */
26
+ sseUrl?: string;
27
+ /** Subscribe POST endpoint. Defaults to `/__relay/subscribe`. */
28
+ subscribeUrl?: string;
29
+ /** Optional bearer token (for guarded relay routes). */
30
+ bearer?: string;
31
+ }
32
+ /**
33
+ * Configure the relay endpoints + bearer. Call once at boot if you
34
+ * need to override the defaults. Multiple calls overwrite — last call
35
+ * wins.
36
+ */
37
+ export declare function configureRelay(options: RelayOptions): void;
38
+ /**
39
+ * Lazily-opened EventSource bound to a single page lifetime. Returns
40
+ * the same client across calls — duplicate `relay()` calls share the
41
+ * underlying connection.
42
+ */
43
+ export declare function relay(): RelayClient;
package/dist/relay.js ADDED
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Browser-side relay helper — hides the EventSource + POST handshake
3
+ * Ream's `@c9up/relay` package expects.
4
+ *
5
+ * import { relay } from '@c9up/aurora/relay'
6
+ *
7
+ * relay().subscribe(`project/${id}`, (ev) => {
8
+ * console.log('received', ev)
9
+ * })
10
+ *
11
+ * One EventSource per page (a singleton inside this module). All
12
+ * subscribe calls fan out to its uid. The connection re-opens after
13
+ * the browser auto-reconnect; subscriptions are re-applied.
14
+ *
15
+ * This module is browser-only. It's shipped via aurora's pre-built
16
+ * `dist/` and imported through the same importmap that maps
17
+ * `@c9up/aurora`. Node-side code that pulls it will trip on
18
+ * `EventSource` being undefined.
19
+ */
20
+ const STATE = {
21
+ sse: null,
22
+ uid: null,
23
+ channels: new Map(),
24
+ pending: [],
25
+ };
26
+ let CONFIG = {
27
+ sseUrl: "/__relay/events",
28
+ subscribeUrl: "/__relay/subscribe",
29
+ bearer: "",
30
+ };
31
+ /**
32
+ * Configure the relay endpoints + bearer. Call once at boot if you
33
+ * need to override the defaults. Multiple calls overwrite — last call
34
+ * wins.
35
+ */
36
+ export function configureRelay(options) {
37
+ CONFIG = {
38
+ sseUrl: options.sseUrl ?? CONFIG.sseUrl,
39
+ subscribeUrl: options.subscribeUrl ?? CONFIG.subscribeUrl,
40
+ bearer: options.bearer ?? CONFIG.bearer,
41
+ };
42
+ }
43
+ /**
44
+ * Lazily-opened EventSource bound to a single page lifetime. Returns
45
+ * the same client across calls — duplicate `relay()` calls share the
46
+ * underlying connection.
47
+ */
48
+ export function relay() {
49
+ if (!STATE.sse) {
50
+ open();
51
+ }
52
+ return CLIENT;
53
+ }
54
+ const CLIENT = {
55
+ subscribe(channel, handler) {
56
+ let handlers = STATE.channels.get(channel);
57
+ if (!handlers) {
58
+ handlers = new Set();
59
+ STATE.channels.set(channel, handlers);
60
+ }
61
+ const adapted = handler;
62
+ handlers.add(adapted);
63
+ // Subscribe over POST as soon as we have a uid. If the SSE is
64
+ // still mid-handshake, queue the call and flush on `connected`.
65
+ const doSubscribe = () => {
66
+ postSubscribe(channel).catch((err) => {
67
+ console.warn(`[aurora/relay] subscribe to ${channel} failed:`, err);
68
+ });
69
+ };
70
+ if (STATE.uid)
71
+ doSubscribe();
72
+ else
73
+ STATE.pending.push(doSubscribe);
74
+ // Detacher — only removes the local listener. The server-side
75
+ // subscription stays open; closing it would interrupt other
76
+ // listeners on the same channel.
77
+ return () => {
78
+ handlers?.delete(adapted);
79
+ };
80
+ },
81
+ close() {
82
+ if (STATE.sse) {
83
+ STATE.sse.close();
84
+ STATE.sse = null;
85
+ }
86
+ STATE.uid = null;
87
+ STATE.channels.clear();
88
+ STATE.pending.length = 0;
89
+ },
90
+ };
91
+ function open() {
92
+ const sse = new EventSource(CONFIG.sseUrl);
93
+ STATE.sse = sse;
94
+ sse.addEventListener("connected", (ev) => {
95
+ const data = safeJson(ev.data);
96
+ if (data && typeof data.uid === "string") {
97
+ STATE.uid = data.uid;
98
+ const queue = STATE.pending.splice(0);
99
+ for (const fn of queue)
100
+ fn();
101
+ }
102
+ });
103
+ sse.onmessage = (ev) => {
104
+ const data = safeJson(ev.data);
105
+ if (!data || typeof data.channel !== "string")
106
+ return;
107
+ const handlers = STATE.channels.get(data.channel);
108
+ if (!handlers)
109
+ return;
110
+ for (const handler of handlers) {
111
+ try {
112
+ handler(data);
113
+ }
114
+ catch (err) {
115
+ console.warn(`[aurora/relay] listener for ${data.channel} threw:`, err);
116
+ }
117
+ }
118
+ };
119
+ }
120
+ async function postSubscribe(channel) {
121
+ const headers = {
122
+ "content-type": "application/json",
123
+ };
124
+ if (CONFIG.bearer)
125
+ headers.authorization = `Bearer ${CONFIG.bearer}`;
126
+ const res = await fetch(CONFIG.subscribeUrl, {
127
+ method: "POST",
128
+ headers,
129
+ body: JSON.stringify({ uid: STATE.uid, channel }),
130
+ });
131
+ if (!res.ok) {
132
+ throw new Error(`HTTP ${res.status}`);
133
+ }
134
+ }
135
+ function safeJson(raw) {
136
+ if (typeof raw !== "string")
137
+ return null;
138
+ try {
139
+ return JSON.parse(raw);
140
+ }
141
+ catch {
142
+ return null;
143
+ }
144
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Render a `TemplateResult` to the DOM and keep it reactive.
3
+ *
4
+ * `render(template, container)` clones the parsed `<template>`, walks to
5
+ * each slot, attaches the corresponding value (with `effect()` for any
6
+ * reactive expression), and appends the result to the container. The
7
+ * returned dispose function tears down every effect and removes the
8
+ * mounted nodes.
9
+ */
10
+ import { type EffectCallback, type TemplateResult } from "./types.js";
11
+ export type Disposer = () => void;
12
+ /**
13
+ * Mount a TemplateResult into `container`. Returns a `Disposer` that
14
+ * stops every reactive effect and removes the mounted nodes. Calling it
15
+ * twice is a no-op.
16
+ */
17
+ export declare function render(result: TemplateResult, container: Element | DocumentFragment): Disposer;
18
+ /**
19
+ * Build a fragment for a TemplateResult and register cleanups.
20
+ *
21
+ * @internal Exported so `hydrate.ts` can client-render a reactive
22
+ * nested-template subtree when the signal changes after hydration
23
+ * (the swap path — see `hydrateTextSlot`).
24
+ */
25
+ export declare function mount(result: TemplateResult, cleanups: Disposer[], mounted: ChildNode[], mountHooks: Array<EffectCallback>): DocumentFragment;