Skip to content

Commit 2c06056

Browse files
authored
Improve the rest of the web components lifecycle. (#582)
1 parent eb7d470 commit 2c06056

File tree

5 files changed

+322
-209
lines changed

5 files changed

+322
-209
lines changed

js/hang/src/meet/element.ts

Lines changed: 84 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Path } from "@kixelated/moq";
2-
import { Effect } from "@kixelated/signals";
2+
import { Effect, Signal } from "@kixelated/signals";
33
import * as DOM from "@kixelated/signals/dom";
44
import { type Publish, Watch } from "..";
55
import { Connection } from "../connection";
@@ -9,12 +9,66 @@ import { Room } from "./room";
99
const OBSERVED = ["url", "name"] as const;
1010
type Observed = (typeof OBSERVED)[number];
1111

12+
export interface HangMeetSignals {
13+
url: Signal<URL | undefined>;
14+
name: Signal<Path.Valid | undefined>;
15+
}
16+
1217
// NOTE: This element is more of an example of how to use the library.
1318
// You likely want your own layout, rendering, controls, etc.
1419
// This element instead creates a crude NxN grid of broadcasts.
1520
export default class HangMeet extends HTMLElement {
1621
static observedAttributes = OBSERVED;
1722

23+
signals: HangMeetSignals = {
24+
url: new Signal<URL | undefined>(undefined),
25+
name: new Signal<Path.Valid | undefined>(undefined),
26+
};
27+
28+
active = new Signal<HangMeetInstance | undefined>(undefined);
29+
30+
connectedCallback() {
31+
this.active.set(new HangMeetInstance(this));
32+
}
33+
34+
disconnectedCallback() {
35+
this.active.set((prev) => {
36+
prev?.close();
37+
return undefined;
38+
});
39+
}
40+
41+
attributeChangedCallback(name: Observed, _oldValue: string | null, newValue: string | null) {
42+
if (name === "url") {
43+
this.url = newValue ? new URL(newValue) : undefined;
44+
} else if (name === "name") {
45+
this.name = newValue ?? undefined;
46+
} else {
47+
const exhaustive: never = name;
48+
throw new Error(`Invalid attribute: ${exhaustive}`);
49+
}
50+
}
51+
52+
get url(): URL | undefined {
53+
return this.signals.url.peek();
54+
}
55+
56+
set url(url: URL | undefined) {
57+
this.signals.url.set(url);
58+
}
59+
60+
get name(): string | undefined {
61+
return this.signals.name.peek()?.toString();
62+
}
63+
64+
set name(name: string | undefined) {
65+
this.signals.name.set(name ? Path.from(name) : undefined);
66+
}
67+
}
68+
69+
class HangMeetInstance {
70+
parent: HangMeet;
71+
1872
connection: Connection;
1973
room: Room;
2074

@@ -31,11 +85,11 @@ export default class HangMeet extends HTMLElement {
3185

3286
#signals = new Effect();
3387

34-
constructor() {
35-
super();
88+
constructor(parent: HangMeet) {
89+
this.parent = parent;
3690

37-
this.connection = new Connection();
38-
this.room = new Room(this.connection);
91+
this.connection = new Connection({ url: this.parent.signals.url });
92+
this.room = new Room(this.connection, { name: this.parent.signals.name });
3993

4094
this.#container = DOM.create("div", {
4195
style: {
@@ -45,18 +99,28 @@ export default class HangMeet extends HTMLElement {
4599
alignItems: "center",
46100
},
47101
});
48-
this.appendChild(this.#container);
102+
103+
DOM.render(this.#signals, this.parent, this.#container);
49104

50105
// A callback that is fired when one of our local broadcasts is added/removed.
51106
this.room.onLocal(this.#onLocal.bind(this));
52107

53108
// A callback that is fired when a remote broadcast is added/removed.
54109
this.room.onRemote(this.#onRemote.bind(this));
110+
111+
this.#signals.effect((effect) => {
112+
// This is kind of a hack to reload the effect when the DOM changes.
113+
const observer = new MutationObserver(() => effect.reload());
114+
observer.observe(this.parent, { childList: true, subtree: true });
115+
effect.cleanup(() => observer.disconnect());
116+
117+
this.#run(effect);
118+
});
55119
}
56120

57-
connectedCallback() {
121+
#run(effect: Effect) {
58122
// Find any nested `hang-publish` elements and mark them as local.
59-
for (const element of this.querySelectorAll("hang-publish")) {
123+
for (const element of this.parent.querySelectorAll("hang-publish")) {
60124
if (!(element instanceof HangPublish)) {
61125
console.warn("hang-publish element not found; tree-shaking?");
62126
continue;
@@ -65,27 +129,25 @@ export default class HangMeet extends HTMLElement {
65129
const publish = element as HangPublish;
66130

67131
// Monitor the name of the publish element and update the room.
68-
this.#signals.effect((effect) => {
69-
const name = effect.get(publish.broadcast.name);
132+
effect.effect((effect) => {
133+
const active = effect.get(publish.active);
134+
if (!active) return;
135+
136+
const name = effect.get(active.broadcast.name);
70137
if (!name) return;
71138

72-
this.room.preview(name, publish.broadcast);
139+
this.room.preview(name, active.broadcast);
73140
effect.cleanup(() => this.room.unpreview(name));
74141
});
75142

76143
// Copy the connection URL to the publish element so they're the same.
77144
// TODO Reuse the connection instead of dialing a new one.
78-
this.#signals.effect((effect) => {
79-
const url = effect.get(this.connection.url);
80-
effect.set(publish.connection.url, url);
145+
effect.effect((effect) => {
146+
publish.url = effect.get(this.connection.url);
81147
});
82148
}
83149
}
84150

85-
disconnectedCallback() {
86-
this.#signals.close();
87-
}
88-
89151
#onLocal(name: Path.Valid, broadcast?: Publish.Broadcast) {
90152
if (!broadcast) {
91153
const existing = this.#locals.get(name);
@@ -152,31 +214,10 @@ export default class HangMeet extends HTMLElement {
152214
this.#container.appendChild(canvas);
153215
}
154216

155-
attributeChangedCallback(name: Observed, _oldValue: string | null, newValue: string | null) {
156-
if (name === "url") {
157-
this.url = newValue ? new URL(newValue) : undefined;
158-
} else if (name === "name") {
159-
this.name = Path.from(newValue ?? "");
160-
} else {
161-
const exhaustive: never = name;
162-
throw new Error(`Invalid attribute: ${exhaustive}`);
163-
}
164-
}
165-
166-
get url(): URL | undefined {
167-
return this.connection.url.peek();
168-
}
169-
170-
set url(url: URL | undefined) {
171-
this.connection.url.set(url);
172-
}
173-
174-
get name(): Path.Valid {
175-
return this.room.name.peek();
176-
}
177-
178-
set name(name: Path.Valid) {
179-
this.room.name.set(name);
217+
close() {
218+
this.#signals.close();
219+
this.room.close();
220+
this.connection.close();
180221
}
181222
}
182223

js/hang/src/meet/room.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import type { Path } from "@kixelated/moq";
22
import { Effect, Signal } from "@kixelated/signals";
3-
import { type Connection, Moq, type Publish, Watch } from "..";
3+
import { type Connection, type Moq, type Publish, Watch } from "..";
44

55
export type Broadcast = Watch.Broadcast | Publish.Broadcast;
66

77
export type RoomProps = {
8-
name?: Path.Valid | Signal<Path.Valid>;
8+
name?: Path.Valid | Signal<Path.Valid | undefined>;
99
};
1010

1111
export class Room {
@@ -14,7 +14,7 @@ export class Room {
1414
connection: Connection;
1515

1616
// An optional prefix to filter broadcasts by.
17-
name: Signal<Path.Valid>;
17+
name: Signal<Path.Valid | undefined>;
1818

1919
// The active broadcasts, sorted by announcement time.
2020
active = new Map<Path.Valid, Broadcast>();
@@ -36,7 +36,7 @@ export class Room {
3636

3737
constructor(connection: Connection, props?: RoomProps) {
3838
this.connection = connection;
39-
this.name = Signal.from(props?.name ?? Moq.Path.empty());
39+
this.name = Signal.from(props?.name);
4040

4141
this.#signals.effect(this.#init.bind(this));
4242
}

0 commit comments

Comments
 (0)