11import { Path } from "@kixelated/moq" ;
2- import { Effect } from "@kixelated/signals" ;
2+ import { Effect , Signal } from "@kixelated/signals" ;
33import * as DOM from "@kixelated/signals/dom" ;
44import { type Publish , Watch } from ".." ;
55import { Connection } from "../connection" ;
@@ -9,12 +9,66 @@ import { Room } from "./room";
99const OBSERVED = [ "url" , "name" ] as const ;
1010type 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.
1520export 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
0 commit comments