@c9up/aurora 0.1.5 → 0.1.6
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/component.js +7 -0
- package/dist/reactive.d.ts +7 -0
- package/dist/reactive.js +25 -1
- package/dist/relay.js +16 -13
- package/dist/server/renderPage.js +1 -1
- package/package.json +1 -1
- package/src/component.ts +7 -0
- package/src/reactive.ts +29 -1
- package/src/relay.ts +19 -11
- package/src/server/renderPage.ts +1 -1
package/dist/component.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* signals the setup function captures. The compiled template is what
|
|
19
19
|
* actually moves on screen.
|
|
20
20
|
*/
|
|
21
|
+
import { setOwner } from "./reactive.js";
|
|
21
22
|
const contextStack = [];
|
|
22
23
|
function activeContext() {
|
|
23
24
|
const ctx = contextStack[contextStack.length - 1];
|
|
@@ -41,11 +42,17 @@ export function component(setup) {
|
|
|
41
42
|
mountHooks: [],
|
|
42
43
|
};
|
|
43
44
|
contextStack.push(ctx);
|
|
45
|
+
// Own the reactive scope of setup: effects/memos created here register
|
|
46
|
+
// their disposer into ctx.cleanups and tear down at unmount, instead of
|
|
47
|
+
// leaking their signal subscriptions. Restored after setup so the outer
|
|
48
|
+
// scope (or none) resumes. Save/restore handles nested component() calls.
|
|
49
|
+
const prevOwner = setOwner(ctx.cleanups);
|
|
44
50
|
try {
|
|
45
51
|
const result = setup((props ?? {}));
|
|
46
52
|
return wrapWithLifecycle(result, ctx);
|
|
47
53
|
}
|
|
48
54
|
finally {
|
|
55
|
+
setOwner(prevOwner);
|
|
49
56
|
contextStack.pop();
|
|
50
57
|
}
|
|
51
58
|
};
|
package/dist/reactive.d.ts
CHANGED
|
@@ -28,6 +28,13 @@ export interface Signal<T> {
|
|
|
28
28
|
* sites need to import this directly.
|
|
29
29
|
*/
|
|
30
30
|
export declare const SIGNAL_BRAND: unique symbol;
|
|
31
|
+
/**
|
|
32
|
+
* @internal Swap the ambient owner, returning the previous one so the
|
|
33
|
+
* caller can restore it. `component()` uses this to own the effects and
|
|
34
|
+
* memos a setup function creates, so they dispose at unmount instead of
|
|
35
|
+
* keeping their signal subscriptions alive forever.
|
|
36
|
+
*/
|
|
37
|
+
export declare function setOwner(owner: Array<() => void> | undefined): Array<() => void> | undefined;
|
|
31
38
|
/**
|
|
32
39
|
* Create a writable signal seeded with `initial`. Reads register the
|
|
33
40
|
* current observer; writes notify every observer that previously read.
|
package/dist/reactive.js
CHANGED
|
@@ -23,6 +23,24 @@ const pendingNotifications = new Set();
|
|
|
23
23
|
function activeObserver() {
|
|
24
24
|
return observerStack[observerStack.length - 1];
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Ambient disposal owner. A non-reactive scope (e.g. a component's setup
|
|
28
|
+
* run) registers an array here so effects/memos created during its
|
|
29
|
+
* execution push their disposer into it and are torn down when the scope
|
|
30
|
+
* ends. `undefined` at top level — no scope, no auto-disposal.
|
|
31
|
+
*/
|
|
32
|
+
let currentOwner;
|
|
33
|
+
/**
|
|
34
|
+
* @internal Swap the ambient owner, returning the previous one so the
|
|
35
|
+
* caller can restore it. `component()` uses this to own the effects and
|
|
36
|
+
* memos a setup function creates, so they dispose at unmount instead of
|
|
37
|
+
* keeping their signal subscriptions alive forever.
|
|
38
|
+
*/
|
|
39
|
+
export function setOwner(owner) {
|
|
40
|
+
const prev = currentOwner;
|
|
41
|
+
currentOwner = owner;
|
|
42
|
+
return prev;
|
|
43
|
+
}
|
|
26
44
|
/**
|
|
27
45
|
* Create a writable signal seeded with `initial`. Reads register the
|
|
28
46
|
* current observer; writes notify every observer that previously read.
|
|
@@ -129,7 +147,13 @@ export function effect(fn) {
|
|
|
129
147
|
},
|
|
130
148
|
};
|
|
131
149
|
eff.run();
|
|
132
|
-
|
|
150
|
+
const dispose = () => eff.dispose();
|
|
151
|
+
// Register with the ambient owner (e.g. a component's setup scope) so the
|
|
152
|
+
// effect is torn down when that scope ends. `memo()` builds on this — its
|
|
153
|
+
// internal recompute effect inherits the same ownership, which is what
|
|
154
|
+
// stops a memo created in component setup from leaking after unmount.
|
|
155
|
+
currentOwner?.push(dispose);
|
|
156
|
+
return dispose;
|
|
133
157
|
}
|
|
134
158
|
/**
|
|
135
159
|
* Register a cleanup callback against the currently-running effect.
|
package/dist/relay.js
CHANGED
|
@@ -21,7 +21,6 @@ const STATE = {
|
|
|
21
21
|
sse: null,
|
|
22
22
|
uid: null,
|
|
23
23
|
channels: new Map(),
|
|
24
|
-
pending: [],
|
|
25
24
|
};
|
|
26
25
|
let CONFIG = {
|
|
27
26
|
sseUrl: "/__relay/events",
|
|
@@ -60,17 +59,15 @@ const CLIENT = {
|
|
|
60
59
|
}
|
|
61
60
|
const adapted = handler;
|
|
62
61
|
handlers.add(adapted);
|
|
63
|
-
// Subscribe over POST as soon as we have a uid.
|
|
64
|
-
//
|
|
65
|
-
|
|
62
|
+
// Subscribe over POST as soon as we have a uid. Before the first uid (or
|
|
63
|
+
// during an auto-reconnect) the channel already lives in STATE.channels
|
|
64
|
+
// and is (re-)subscribed by the `connected` handler — so the server,
|
|
65
|
+
// which assigns a fresh uid per connection, always learns every channel.
|
|
66
|
+
if (STATE.uid) {
|
|
66
67
|
postSubscribe(channel).catch((err) => {
|
|
67
68
|
console.warn(`[aurora/relay] subscribe to ${channel} failed:`, err);
|
|
68
69
|
});
|
|
69
|
-
}
|
|
70
|
-
if (STATE.uid)
|
|
71
|
-
doSubscribe();
|
|
72
|
-
else
|
|
73
|
-
STATE.pending.push(doSubscribe);
|
|
70
|
+
}
|
|
74
71
|
// Detacher — only removes the local listener. The server-side
|
|
75
72
|
// subscription stays open; closing it would interrupt other
|
|
76
73
|
// listeners on the same channel.
|
|
@@ -85,7 +82,6 @@ const CLIENT = {
|
|
|
85
82
|
}
|
|
86
83
|
STATE.uid = null;
|
|
87
84
|
STATE.channels.clear();
|
|
88
|
-
STATE.pending.length = 0;
|
|
89
85
|
},
|
|
90
86
|
};
|
|
91
87
|
function open() {
|
|
@@ -95,9 +91,16 @@ function open() {
|
|
|
95
91
|
const data = safeJson(ev.data);
|
|
96
92
|
if (data && typeof data.uid === "string") {
|
|
97
93
|
STATE.uid = data.uid;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
94
|
+
// Re-apply EVERY active subscription on each (re)connect. The server
|
|
95
|
+
// assigns a fresh uid per connection and has no memory of prior
|
|
96
|
+
// subscriptions, so both the first connect AND browser auto-reconnects
|
|
97
|
+
// must re-POST every live channel — otherwise the client silently
|
|
98
|
+
// stops receiving after a reconnect.
|
|
99
|
+
for (const channel of STATE.channels.keys()) {
|
|
100
|
+
postSubscribe(channel).catch((err) => {
|
|
101
|
+
console.warn(`[aurora/relay] re-subscribe to ${channel} failed:`, err);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
101
104
|
}
|
|
102
105
|
});
|
|
103
106
|
sse.onmessage = (ev) => {
|
|
@@ -41,7 +41,7 @@ export async function renderPage(ctx, pages, name, props, options = {}) {
|
|
|
41
41
|
<head>
|
|
42
42
|
<meta charset="utf-8" />
|
|
43
43
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
44
|
-
<script type="importmap">${
|
|
44
|
+
<script type="importmap">${escapeJsonForScript({ imports: importmap })}</script>
|
|
45
45
|
${options.headExtra ?? ""}
|
|
46
46
|
</head>
|
|
47
47
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c9up/aurora",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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/component.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* actually moves on screen.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
import { setOwner } from "./reactive.js";
|
|
22
23
|
import type { Disposer } from "./render.js";
|
|
23
24
|
import type { EffectCallback, TemplateResult } from "./types.js";
|
|
24
25
|
|
|
@@ -63,10 +64,16 @@ export function component<P = Record<string, never>>(
|
|
|
63
64
|
mountHooks: [],
|
|
64
65
|
};
|
|
65
66
|
contextStack.push(ctx);
|
|
67
|
+
// Own the reactive scope of setup: effects/memos created here register
|
|
68
|
+
// their disposer into ctx.cleanups and tear down at unmount, instead of
|
|
69
|
+
// leaking their signal subscriptions. Restored after setup so the outer
|
|
70
|
+
// scope (or none) resumes. Save/restore handles nested component() calls.
|
|
71
|
+
const prevOwner = setOwner(ctx.cleanups);
|
|
66
72
|
try {
|
|
67
73
|
const result = setup((props ?? ({} as P)) as P);
|
|
68
74
|
return wrapWithLifecycle(result, ctx);
|
|
69
75
|
} finally {
|
|
76
|
+
setOwner(prevOwner);
|
|
70
77
|
contextStack.pop();
|
|
71
78
|
}
|
|
72
79
|
};
|
package/src/reactive.ts
CHANGED
|
@@ -64,6 +64,28 @@ function activeObserver(): Effect | undefined {
|
|
|
64
64
|
return observerStack[observerStack.length - 1];
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Ambient disposal owner. A non-reactive scope (e.g. a component's setup
|
|
69
|
+
* run) registers an array here so effects/memos created during its
|
|
70
|
+
* execution push their disposer into it and are torn down when the scope
|
|
71
|
+
* ends. `undefined` at top level — no scope, no auto-disposal.
|
|
72
|
+
*/
|
|
73
|
+
let currentOwner: Array<() => void> | undefined;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @internal Swap the ambient owner, returning the previous one so the
|
|
77
|
+
* caller can restore it. `component()` uses this to own the effects and
|
|
78
|
+
* memos a setup function creates, so they dispose at unmount instead of
|
|
79
|
+
* keeping their signal subscriptions alive forever.
|
|
80
|
+
*/
|
|
81
|
+
export function setOwner(
|
|
82
|
+
owner: Array<() => void> | undefined,
|
|
83
|
+
): Array<() => void> | undefined {
|
|
84
|
+
const prev = currentOwner;
|
|
85
|
+
currentOwner = owner;
|
|
86
|
+
return prev;
|
|
87
|
+
}
|
|
88
|
+
|
|
67
89
|
/**
|
|
68
90
|
* Create a writable signal seeded with `initial`. Reads register the
|
|
69
91
|
* current observer; writes notify every observer that previously read.
|
|
@@ -178,7 +200,13 @@ export function effect(fn: EffectCallback): () => void {
|
|
|
178
200
|
},
|
|
179
201
|
};
|
|
180
202
|
eff.run();
|
|
181
|
-
|
|
203
|
+
const dispose = () => eff.dispose();
|
|
204
|
+
// Register with the ambient owner (e.g. a component's setup scope) so the
|
|
205
|
+
// effect is torn down when that scope ends. `memo()` builds on this — its
|
|
206
|
+
// internal recompute effect inherits the same ownership, which is what
|
|
207
|
+
// stops a memo created in component setup from leaking after unmount.
|
|
208
|
+
currentOwner?.push(dispose);
|
|
209
|
+
return dispose;
|
|
182
210
|
}
|
|
183
211
|
|
|
184
212
|
/**
|
package/src/relay.ts
CHANGED
|
@@ -27,14 +27,12 @@ interface RelayState {
|
|
|
27
27
|
sse: EventSource | null;
|
|
28
28
|
uid: string | null;
|
|
29
29
|
channels: Map<string, Set<(event: unknown) => void>>;
|
|
30
|
-
pending: Array<() => void>;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
const STATE: RelayState = {
|
|
34
33
|
sse: null,
|
|
35
34
|
uid: null,
|
|
36
35
|
channels: new Map(),
|
|
37
|
-
pending: [],
|
|
38
36
|
};
|
|
39
37
|
|
|
40
38
|
export interface RelayOptions {
|
|
@@ -87,15 +85,15 @@ const CLIENT: RelayClient = {
|
|
|
87
85
|
const adapted = handler as (event: unknown) => void;
|
|
88
86
|
handlers.add(adapted);
|
|
89
87
|
|
|
90
|
-
// Subscribe over POST as soon as we have a uid.
|
|
91
|
-
//
|
|
92
|
-
|
|
88
|
+
// Subscribe over POST as soon as we have a uid. Before the first uid (or
|
|
89
|
+
// during an auto-reconnect) the channel already lives in STATE.channels
|
|
90
|
+
// and is (re-)subscribed by the `connected` handler — so the server,
|
|
91
|
+
// which assigns a fresh uid per connection, always learns every channel.
|
|
92
|
+
if (STATE.uid) {
|
|
93
93
|
postSubscribe(channel).catch((err: unknown) => {
|
|
94
94
|
console.warn(`[aurora/relay] subscribe to ${channel} failed:`, err);
|
|
95
95
|
});
|
|
96
|
-
}
|
|
97
|
-
if (STATE.uid) doSubscribe();
|
|
98
|
-
else STATE.pending.push(doSubscribe);
|
|
96
|
+
}
|
|
99
97
|
|
|
100
98
|
// Detacher — only removes the local listener. The server-side
|
|
101
99
|
// subscription stays open; closing it would interrupt other
|
|
@@ -112,7 +110,6 @@ const CLIENT: RelayClient = {
|
|
|
112
110
|
}
|
|
113
111
|
STATE.uid = null;
|
|
114
112
|
STATE.channels.clear();
|
|
115
|
-
STATE.pending.length = 0;
|
|
116
113
|
},
|
|
117
114
|
};
|
|
118
115
|
|
|
@@ -124,8 +121,19 @@ function open(): void {
|
|
|
124
121
|
const data = safeJson<{ uid?: string }>((ev as MessageEvent).data);
|
|
125
122
|
if (data && typeof data.uid === "string") {
|
|
126
123
|
STATE.uid = data.uid;
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
// Re-apply EVERY active subscription on each (re)connect. The server
|
|
125
|
+
// assigns a fresh uid per connection and has no memory of prior
|
|
126
|
+
// subscriptions, so both the first connect AND browser auto-reconnects
|
|
127
|
+
// must re-POST every live channel — otherwise the client silently
|
|
128
|
+
// stops receiving after a reconnect.
|
|
129
|
+
for (const channel of STATE.channels.keys()) {
|
|
130
|
+
postSubscribe(channel).catch((err: unknown) => {
|
|
131
|
+
console.warn(
|
|
132
|
+
`[aurora/relay] re-subscribe to ${channel} failed:`,
|
|
133
|
+
err,
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
129
137
|
}
|
|
130
138
|
});
|
|
131
139
|
|
package/src/server/renderPage.ts
CHANGED
|
@@ -90,7 +90,7 @@ export async function renderPage<P>(
|
|
|
90
90
|
<head>
|
|
91
91
|
<meta charset="utf-8" />
|
|
92
92
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
93
|
-
<script type="importmap">${
|
|
93
|
+
<script type="importmap">${escapeJsonForScript({ imports: importmap })}</script>
|
|
94
94
|
${options.headExtra ?? ""}
|
|
95
95
|
</head>
|
|
96
96
|
<body>
|