github.com/vugu/vugu@v0.3.6-0.20240430171613-3f6f402e014b/domrender/renderer-js.go (about)

     1  package domrender
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/binary"
     6  	"errors"
     7  	"fmt"
     8  	"strconv"
     9  	"strings"
    10  	"sync"
    11  
    12  	"github.com/vugu/vjson"
    13  
    14  	"github.com/vugu/vugu"
    15  
    16  	js "github.com/vugu/vugu/js"
    17  )
    18  
    19  //go:generate go run renderer-js-script-maker.go
    20  
    21  // NewJSRenderer is an alias for New.
    22  //
    23  // Deprecated: Use New instead.
    24  func NewJSRenderer(mountPointSelector string) (*JSRenderer, error) {
    25  	return New(mountPointSelector)
    26  }
    27  
    28  // New will create a new JSRenderer with the speicifc mount point selector.
    29  // If an empty string is passed then the root component should include a top level <html> tag
    30  // and the entire page will be rendered.
    31  func New(mountPointSelector string) (*JSRenderer, error) {
    32  
    33  	ret := &JSRenderer{
    34  		MountPointSelector: mountPointSelector,
    35  	}
    36  
    37  	ret.instructionBuffer = make([]byte, 16384)
    38  	// ret.instructionTypedArray = js.TypedArrayOf(ret.instructionBuffer)
    39  
    40  	ret.window = js.Global().Get("window")
    41  
    42  	ret.window.Call("eval", jsHelperScript)
    43  
    44  	ret.instructionBufferJS = ret.window.Call("vuguGetRenderArray")
    45  
    46  	ret.instructionList = newInstructionList(ret.instructionBuffer, func(il *instructionList) error {
    47  
    48  		// call vuguRender to have the instructions processed in JS
    49  		ret.instructionBuffer[il.pos] = 0 // ensure zero terminator
    50  
    51  		// copy the data over
    52  		js.CopyBytesToJS(ret.instructionBufferJS, ret.instructionBuffer)
    53  
    54  		// then call vuguRender
    55  		ret.window.Call("vuguRender" /*, ret.instructionBufferJS*/)
    56  
    57  		return nil
    58  	})
    59  
    60  	// enable debug logging
    61  	// ret.instructionList.logWriter = os.Stdout
    62  
    63  	ret.eventHandlerBuffer = make([]byte, 16384)
    64  	// ret.eventHandlerTypedArray = js.TypedArrayOf(ret.eventHandlerBuffer)
    65  
    66  	ret.eventHandlerFunc = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
    67  		if len(args) != 1 {
    68  			panic(fmt.Errorf("eventHandlerFunc got arg slice not exactly 1 element in length: %#v", args))
    69  		}
    70  		bufferLength := args[0].Length()
    71  		if cap(ret.eventHandlerBuffer) < bufferLength+1 {
    72  			ret.eventHandlerBuffer = make([]byte, bufferLength+1)
    73  		}
    74  		//log.Println(cap(ret.eventHandlerBuffer))
    75  		n := js.CopyBytesToGo(ret.eventHandlerBuffer, args[0])
    76  		if n >= len(ret.eventHandlerBuffer) {
    77  			panic(errors.New("event data is too large, cannot continue, len: " + strconv.Itoa(n)))
    78  		}
    79  		ret.handleDOMEvent() // discard this and args, all data should be in eventHandlerBuffer; avoid using js.Value
    80  		return nil
    81  		// return jsEnv.handleRawDOMEvent(this, args)
    82  	})
    83  
    84  	// wire up the event handler func and the array that we used to communicate with instead of js.Value
    85  	// ret.window.Call("vuguSetEventHandlerAndBuffer", ret.eventHandlerFunc, ret.eventHandlerTypedArray)
    86  
    87  	// wire up callback handler
    88  	ret.window.Call("vuguSetCallbackHandler", js.FuncOf(ret.handleCallback))
    89  
    90  	// wire up the event handler func
    91  	ret.window.Call("vuguSetEventHandler", ret.eventHandlerFunc)
    92  
    93  	// log.Printf("ret.window: %#v", ret.window)
    94  	// log.Printf("eval: %#v", ret.window.Get("eval"))
    95  
    96  	ret.eventWaitCh = make(chan bool, 64)
    97  
    98  	ret.eventEnv = vugu.NewEventEnvImpl(
    99  		&ret.eventRWMU,
   100  		ret.eventWaitCh,
   101  	)
   102  
   103  	return ret, nil
   104  }
   105  
   106  type jsRenderState struct {
   107  	// stores positionID to slice of DOMEventHandlerSpec
   108  	domHandlerMap map[string][]vugu.DOMEventHandlerSpec
   109  
   110  	// callback stuff is handled by callbackManager
   111  	callbackManager callbackManager
   112  }
   113  
   114  func newJsRenderState() *jsRenderState {
   115  	return &jsRenderState{
   116  		domHandlerMap: make(map[string][]vugu.DOMEventHandlerSpec, 8),
   117  	}
   118  }
   119  
   120  // JSRenderer implements Renderer against the browser's DOM.
   121  type JSRenderer struct {
   122  	MountPointSelector string
   123  
   124  	eventWaitCh chan bool          // events send to this and EventWait receives from it
   125  	eventRWMU   sync.RWMutex       // make sure Render and event handling are not attempted at the same time (not totally sure if this is necessary in terms of the wasm threading model but enforce it with a rwmutex all the same)
   126  	eventEnv    *vugu.EventEnvImpl // our EventEnv implementation that exposes eventRWMU and eventWaitCh to events in a clean way
   127  
   128  	eventHandlerFunc   js.Func // the callback function for DOM events
   129  	eventHandlerBuffer []byte
   130  	// eventHandlerTypedArray js.TypedArray
   131  
   132  	instructionBuffer   []byte   // our local instruction buffer
   133  	instructionBufferJS js.Value // a Uint8Array on the JS side that we copy into
   134  	instructionList     *instructionList
   135  
   136  	window js.Value
   137  
   138  	jsRenderState *jsRenderState
   139  
   140  	// manages the Rendered lifecycle callback stuff
   141  	lifecycleStateMap map[interface{}]lifecycleState
   142  	lifecyclePassNum  uint8
   143  }
   144  
   145  type lifecycleState struct {
   146  	passNum uint8
   147  }
   148  
   149  // EventEnv returns an EventEnv that can be used for synchronizing updates.
   150  func (r *JSRenderer) EventEnv() vugu.EventEnv {
   151  	return r.eventEnv
   152  }
   153  
   154  // Release calls release on any resources that this renderer allocated.
   155  func (r *JSRenderer) Release() {
   156  	// NOTE: seems sensible to leave this here in case we do need something to be released, better than
   157  	// omitting it and people getting used to no release being needed and then requiring it later.
   158  	// r.instructionTypedArray.Release()
   159  }
   160  
   161  func (r *JSRenderer) render(buildResults *vugu.BuildResults) error {
   162  
   163  	bo := buildResults.Out
   164  
   165  	if !js.Global().Truthy() {
   166  		return errors.New("js environment not available")
   167  	}
   168  
   169  	if bo == nil {
   170  		return errors.New("BuildOut is nil")
   171  	}
   172  
   173  	if len(bo.Out) != 1 {
   174  		return errors.New("BuildOut.Out has bad len " + strconv.Itoa(len(bo.Out)))
   175  	}
   176  
   177  	if bo.Out[0].Type != vugu.ElementNode {
   178  		return errors.New("BuildOut.Out[0].Type is not vugu.ElementNode: " + strconv.Itoa(int(bo.Out[0].Type)))
   179  	}
   180  
   181  	// always make sure we have at least a non-nil render state
   182  	if r.jsRenderState == nil {
   183  		r.jsRenderState = newJsRenderState()
   184  	}
   185  
   186  	state := r.jsRenderState
   187  
   188  	state.callbackManager.startRender()
   189  	defer state.callbackManager.doneRender()
   190  
   191  	// TODO: move this next chunk out to it's own func at least
   192  
   193  	visitCSSList := func(cssList []*vugu.VGNode) error {
   194  		// CSS stuff first
   195  		for _, cssEl := range cssList {
   196  
   197  			// some basic sanity checking
   198  			if cssEl.Type != vugu.ElementNode || !(cssEl.Data == "style" || cssEl.Data == "link") {
   199  				return errors.New("CSS output must be link or style tag")
   200  			}
   201  
   202  			var textBuf bytes.Buffer
   203  			for childN := cssEl.FirstChild; childN != nil; childN = childN.NextSibling {
   204  				if childN.Type != vugu.TextNode {
   205  					return fmt.Errorf("CSS tag must contain only text children, found %v instead: %#v", childN.Type, childN)
   206  				}
   207  				textBuf.WriteString(childN.Data)
   208  			}
   209  
   210  			var attrPairs []string
   211  			if len(cssEl.Attr) > 0 {
   212  				attrPairs = make([]string, 0, len(cssEl.Attr)*2)
   213  				for _, attr := range cssEl.Attr {
   214  					attrPairs = append(attrPairs, attr.Key, attr.Val)
   215  				}
   216  			}
   217  
   218  			err := r.instructionList.writeSetCSSTag(cssEl.Data, textBuf.Bytes(), attrPairs)
   219  			if err != nil {
   220  				return err
   221  			}
   222  		}
   223  
   224  		return nil
   225  	}
   226  
   227  	var walkCSSBuildOut func(buildOut *vugu.BuildOut) error
   228  	walkCSSBuildOut = func(buildOut *vugu.BuildOut) error {
   229  		err := visitCSSList(buildOut.CSS)
   230  		if err != nil {
   231  			return err
   232  		}
   233  		for _, c := range buildOut.Components {
   234  			// nextBuildOut := buildResults.AllOut[c]
   235  			nextBuildOut := buildResults.ResultFor(c)
   236  			if nextBuildOut == nil {
   237  				panic(fmt.Errorf("walkCSSBuildOut nextBuildOut was nil for %#v", c))
   238  			}
   239  			err := walkCSSBuildOut(nextBuildOut)
   240  			if err != nil {
   241  				return err
   242  			}
   243  		}
   244  		return nil
   245  	}
   246  	err := walkCSSBuildOut(bo)
   247  	if err != nil {
   248  		return err
   249  	}
   250  
   251  	err = r.instructionList.writeRemoveOtherCSSTags()
   252  	if err != nil {
   253  		return err
   254  	}
   255  
   256  	// main output
   257  	err = r.visitFirst(state, bo, buildResults, bo.Out[0], []byte("0"))
   258  	if err != nil {
   259  		return err
   260  	}
   261  
   262  	// // JS stuff last
   263  	// // log.Printf("TODO: handle JS")
   264  
   265  	err = r.instructionList.flush()
   266  	if err != nil {
   267  		return err
   268  	}
   269  
   270  	// handle Rendered lifecycle callback
   271  	if r.lifecycleStateMap == nil {
   272  		r.lifecycleStateMap = make(map[interface{}]lifecycleState, len(bo.Components))
   273  	}
   274  	r.lifecyclePassNum++
   275  
   276  	var rctx renderedCtx
   277  
   278  	for _, c := range bo.Components {
   279  
   280  		rctx = renderedCtx{eventEnv: r.eventEnv}
   281  
   282  		st, ok := r.lifecycleStateMap[c]
   283  		rctx.first = !ok
   284  		st.passNum = r.lifecyclePassNum
   285  
   286  		invokeRendered(c, &rctx)
   287  
   288  		r.lifecycleStateMap[c] = st
   289  
   290  	}
   291  
   292  	// now purge from lifecycleStateMap anything not touched in this pass
   293  	for k, st := range r.lifecycleStateMap {
   294  		if st.passNum != r.lifecyclePassNum {
   295  			delete(r.lifecycleStateMap, k)
   296  		}
   297  	}
   298  
   299  	return nil
   300  
   301  }
   302  
   303  // EventWait blocks until an event has occurred which causes a re-render.
   304  // It returns true if the render loop should continue or false if it should exit.
   305  func (r *JSRenderer) EventWait() (ok bool) {
   306  
   307  	// make sure the JS environment is still available, returning false otherwise
   308  	if !js.Global().Truthy() {
   309  		return false
   310  	}
   311  
   312  	// FIXME: this should probably have some sort of "debouncing" on it to handle the case of
   313  	// several events in rapid succession causing multiple renders - maybe we read from eventWaitCH
   314  	// continuously until it's empty, with a max of like 20ms pause between each or something, and then
   315  	// only return after we don't see anything for that time frame.
   316  
   317  	ok = <-r.eventWaitCh
   318  	return
   319  
   320  }
   321  
   322  // var window js.Value
   323  
   324  // func init() {
   325  // 	window = js.Global().Get("window")
   326  // 	if window.Truthy() {
   327  // 		js.Global().Call("eval", jsHelperScript)
   328  // 	}
   329  // }
   330  
   331  func (r *JSRenderer) visitFirst(state *jsRenderState, bo *vugu.BuildOut, br *vugu.BuildResults, n *vugu.VGNode, positionID []byte) error {
   332  
   333  	// log.Printf("TODO: We need to go through and optimize away unneeded calls to create elements, set attributes, set event handlers, etc. for cases where they are the same per hash")
   334  
   335  	// log.Printf("JSRenderer.visitFirst")
   336  
   337  	if n.Type != vugu.ElementNode {
   338  		return errors.New("root of component must be element")
   339  	}
   340  
   341  	err := r.instructionList.writeClearEl()
   342  	if err != nil {
   343  		return err
   344  	}
   345  
   346  	// first tag is html
   347  	if strings.ToLower(n.Data) == "html" {
   348  
   349  		err := r.syncHtml(state, n, []byte("html"))
   350  		if err != nil {
   351  			return err
   352  		}
   353  
   354  		for nchild := n.FirstChild; nchild != nil; nchild = nchild.NextSibling {
   355  
   356  			if strings.ToLower(nchild.Data) == "head" {
   357  
   358  				err := r.visitHead(state, bo, br, nchild, []byte("head"))
   359  				if err != nil {
   360  					return err
   361  				}
   362  
   363  			} else if strings.ToLower(nchild.Data) == "body" {
   364  
   365  				err := r.visitBody(state, bo, br, nchild, []byte("body"))
   366  				if err != nil {
   367  					return err
   368  				}
   369  
   370  			} else {
   371  				return fmt.Errorf("unexpected tag inside html %q (VGNode=%#v)", nchild.Data, nchild)
   372  			}
   373  
   374  		}
   375  
   376  		return nil
   377  	}
   378  
   379  	// else, first tag is anything else - try again as the element to be mounted
   380  	return r.visitMount(state, bo, br, n, positionID)
   381  
   382  }
   383  
   384  func (r *JSRenderer) syncHtml(state *jsRenderState, n *vugu.VGNode, positionID []byte) error {
   385  	err := r.instructionList.writeSelectQuery("html")
   386  	if err != nil {
   387  		return err
   388  	}
   389  	return r.syncElement(state, n, positionID)
   390  }
   391  
   392  func (r *JSRenderer) visitHead(state *jsRenderState, bo *vugu.BuildOut, br *vugu.BuildResults, n *vugu.VGNode, positionID []byte) error {
   393  
   394  	err := r.instructionList.writeSelectQuery("head")
   395  	if err != nil {
   396  		return err
   397  	}
   398  	err = r.syncElement(state, n, positionID)
   399  	if err != nil {
   400  		return err
   401  	}
   402  
   403  	return nil
   404  }
   405  
   406  func (r *JSRenderer) visitBody(state *jsRenderState, bo *vugu.BuildOut, br *vugu.BuildResults, n *vugu.VGNode, positionID []byte) error {
   407  
   408  	err := r.instructionList.writeSelectQuery("body")
   409  	if err != nil {
   410  		return err
   411  	}
   412  	err = r.syncElement(state, n, positionID)
   413  	if err != nil {
   414  		return err
   415  	}
   416  
   417  	if !(n.FirstChild != nil && n.FirstChild.NextSibling == nil) {
   418  		return errors.New("body tag must contain exactly one element child")
   419  	}
   420  
   421  	return r.visitMount(state, bo, br, n.FirstChild, positionID)
   422  }
   423  
   424  func (r *JSRenderer) visitMount(state *jsRenderState, bo *vugu.BuildOut, br *vugu.BuildResults, n *vugu.VGNode, positionID []byte) error {
   425  
   426  	// log.Printf("visitMount got here")
   427  
   428  	err := r.instructionList.writeSelectMountPoint(r.MountPointSelector, n.Data)
   429  	if err != nil {
   430  		return err
   431  	}
   432  
   433  	return r.visitSyncElementEtc(state, bo, br, n, positionID)
   434  
   435  }
   436  
   437  func (r *JSRenderer) visitSyncNode(state *jsRenderState, bo *vugu.BuildOut, br *vugu.BuildResults, n *vugu.VGNode, positionID []byte) error {
   438  
   439  	// log.Printf("visitSyncNode")
   440  
   441  	var err error
   442  
   443  	// check for Component, in which case we descend into it instead of processing like a regular node
   444  	if n.Component != nil {
   445  		compBuildOut := br.ResultFor(n.Component)
   446  		if len(compBuildOut.Out) != 1 {
   447  			return fmt.Errorf("component %#v expected exactly one Out element but got %d instead",
   448  				n.Component, len(compBuildOut.Out))
   449  		}
   450  		return r.visitSyncNode(state, compBuildOut, br, compBuildOut.Out[0], positionID)
   451  	}
   452  
   453  	// check for template (used by vg-template and vg-slot) in which case we process the children directly and ignore n
   454  	if n.IsTemplate() {
   455  
   456  		childIndex := 1
   457  		for nchild := n.FirstChild; nchild != nil; nchild = nchild.NextSibling {
   458  
   459  			// use a different character here for the position to ensure it's unique
   460  			childPositionID := append(positionID, []byte(fmt.Sprintf("_t_%d", childIndex))...)
   461  
   462  			err = r.visitSyncNode(state, bo, br, nchild, childPositionID)
   463  			if err != nil {
   464  				return err
   465  			}
   466  
   467  			// if there are more children, advance to the next
   468  			if nchild.NextSibling != nil {
   469  				err = r.instructionList.writeMoveToNextSibling()
   470  				if err != nil {
   471  					return err
   472  				}
   473  
   474  			}
   475  
   476  			childIndex++
   477  		}
   478  
   479  		// element is fully handled
   480  		return nil
   481  	}
   482  
   483  	switch n.Type {
   484  	case vugu.ElementNode:
   485  		// check if this element has a namespace set
   486  		if ns := namespaceToURI(n.Namespace); ns != "" {
   487  			err = r.instructionList.writeSetElementNS(n.Data, ns)
   488  		} else {
   489  			err = r.instructionList.writeSetElement(n.Data)
   490  		}
   491  		if err != nil {
   492  			return err
   493  		}
   494  	case vugu.TextNode:
   495  		return r.instructionList.writeSetText(n.Data) // no children possible, just return
   496  	case vugu.CommentNode:
   497  		return r.instructionList.writeSetComment(n.Data) // no children possible, just return
   498  	default:
   499  		return errors.New("unknown node type: " + strconv.Itoa(int(n.Type)))
   500  	}
   501  
   502  	// only elements have attributes, child or events
   503  	return r.visitSyncElementEtc(state, bo, br, n, positionID)
   504  
   505  }
   506  
   507  // visitSyncElementEtc syncs the rest of the stuff that only applies to elements
   508  func (r *JSRenderer) visitSyncElementEtc(state *jsRenderState, bo *vugu.BuildOut, br *vugu.BuildResults, n *vugu.VGNode, positionID []byte) error {
   509  
   510  	err := r.syncElement(state, n, positionID)
   511  	if err != nil {
   512  		return err
   513  	}
   514  
   515  	if n.InnerHTML != nil {
   516  		return r.instructionList.writeSetInnerHTML(*n.InnerHTML)
   517  	}
   518  
   519  	// tell callbackManager about the create and populate functions
   520  	// (if present, otherwise this is a nop and will return 0,0)
   521  	cid, pid := state.callbackManager.addCreateAndPopulateHandlers(n.JSCreateHandler, n.JSPopulateHandler)
   522  
   523  	// for vg-js-create, send an instruction to call us back when this element is created
   524  	// (handled by callbackManager)
   525  	if cid != 0 {
   526  		err := r.instructionList.writeCallbackLastElement(cid)
   527  		if err != nil {
   528  			return err
   529  		}
   530  	}
   531  
   532  	if n.FirstChild != nil {
   533  
   534  		err = r.instructionList.writeMoveToFirstChild()
   535  		if err != nil {
   536  			return err
   537  		}
   538  
   539  		childIndex := 1
   540  		for nchild := n.FirstChild; nchild != nil; nchild = nchild.NextSibling {
   541  
   542  			childPositionID := append(positionID, []byte(fmt.Sprintf("_%d", childIndex))...)
   543  
   544  			err = r.visitSyncNode(state, bo, br, nchild, childPositionID)
   545  			if err != nil {
   546  				return err
   547  			}
   548  			// log.Printf("GOT HERE X: %#v", n)
   549  			err = r.instructionList.writeMoveToNextSibling()
   550  			if err != nil {
   551  				return err
   552  			}
   553  			childIndex++
   554  		}
   555  
   556  		err = r.instructionList.writeMoveToParent()
   557  		if err != nil {
   558  			return err
   559  		}
   560  	}
   561  
   562  	// for vg-js-populate, send an instruction to call us back again with the populate flag for this same one
   563  	// (handled by callbackManager)
   564  	if pid != 0 {
   565  		err := r.instructionList.writeCallback(pid)
   566  		if err != nil {
   567  			return err
   568  		}
   569  	}
   570  
   571  	return nil
   572  }
   573  
   574  func (r *JSRenderer) syncElement(state *jsRenderState, n *vugu.VGNode, positionID []byte) error {
   575  	if namespaceToURI(n.Namespace) != "" {
   576  		for _, a := range n.Attr {
   577  			ns := namespaceToURI(a.Namespace)
   578  			// FIXME: we skip Namespace="" && Key = "xmlns" here, because this WILL cause an js exception
   579  			// the correct way would be, to parse the xmlns attribute in the generator, set the namespace of the holding element
   580  			// and then forget about this attribute
   581  			if ns == "" && a.Key == "xmlns" {
   582  				continue
   583  			}
   584  			err := r.instructionList.writeSetAttrNSStr(ns, a.Key, a.Val)
   585  			if err != nil {
   586  				return err
   587  			}
   588  		}
   589  	} else {
   590  		for _, a := range n.Attr {
   591  			err := r.instructionList.writeSetAttrStr(a.Key, a.Val)
   592  			if err != nil {
   593  				return err
   594  			}
   595  		}
   596  	}
   597  
   598  	err := r.instructionList.writeRemoveOtherAttrs()
   599  	if err != nil {
   600  		return err
   601  	}
   602  
   603  	// do any JS properties
   604  	for _, p := range n.Prop {
   605  		err := r.instructionList.writeSetProperty(p.Key, []byte(p.JSONVal))
   606  		if err != nil {
   607  			return err
   608  		}
   609  	}
   610  
   611  	if len(n.DOMEventHandlerSpecList) > 0 {
   612  
   613  		// store in domHandlerMap
   614  		state.domHandlerMap[string(positionID)] = n.DOMEventHandlerSpecList
   615  
   616  		for _, hs := range n.DOMEventHandlerSpecList {
   617  			err := r.instructionList.writeSetEventListener(positionID, hs.EventType, hs.Capture, hs.Passive)
   618  			if err != nil {
   619  				return err
   620  			}
   621  		}
   622  	}
   623  	// always write the remove for event listeners so any previous ones are taken away
   624  	return r.instructionList.writeRemoveOtherEventListeners(positionID)
   625  }
   626  
   627  // // writeAllStaticAttrs is a helper to write all the static attrs from a VGNode
   628  // func (r *JSRenderer) writeAllStaticAttrs(n *vugu.VGNode) error {
   629  // 	for _, a := range n.Attr {
   630  // 		err := r.instructionList.writeSetAttrStr(a.Key, a.Val)
   631  // 		if err != nil {
   632  // 			return err
   633  // 		}
   634  // 	}
   635  // 	return nil
   636  // }
   637  
   638  func (r *JSRenderer) handleCallback(this js.Value, args []js.Value) interface{} {
   639  	return r.jsRenderState.callbackManager.callback(this, args)
   640  }
   641  
   642  func (r *JSRenderer) handleDOMEvent() {
   643  
   644  	strlen := binary.BigEndian.Uint32(r.eventHandlerBuffer[:4])
   645  	b := r.eventHandlerBuffer[4 : strlen+4]
   646  	// log.Printf("handleDOMEvent JSON from event buffer: %q", b)
   647  
   648  	// var ee eventEnv
   649  	// rwmu            *sync.RWMutex
   650  	// requestRenderCH chan bool
   651  
   652  	var eventDetail struct {
   653  		PositionID string // `json:"position_id"`
   654  		EventType  string // `json:"event_type"`
   655  		Capture    bool   // `json:"capture"`
   656  		Passive    bool   // `json:"passive"`
   657  
   658  		// the event object data as extracted above
   659  		EventSummary map[string]interface{} // `json:"event_summary"`
   660  	}
   661  
   662  	edm := make(map[string]interface{}, 6)
   663  	// err := json.Unmarshal(b, &eventDetail)
   664  	err := vjson.Unmarshal(b, &edm)
   665  	if err != nil {
   666  		panic(err)
   667  	}
   668  
   669  	// manually extract fields
   670  	eventDetail.PositionID, _ = edm["position_id"].(string)
   671  	eventDetail.EventType, _ = edm["event_type"].(string)
   672  	eventDetail.Capture, _ = edm["capture"].(bool)
   673  	eventDetail.Passive, _ = edm["passive"].(bool)
   674  	eventDetail.EventSummary, _ = edm["event_summary"].(map[string]interface{})
   675  
   676  	domEvent := vugu.NewDOMEvent(r.eventEnv, eventDetail.EventSummary)
   677  
   678  	// log.Printf("eventDetail: %#v", eventDetail)
   679  
   680  	// it is important that we lock around accessing anything that might change (domHandlerMap)
   681  	// and around the invokation of the handler call itself
   682  
   683  	r.eventRWMU.Lock()
   684  	handlers := r.jsRenderState.domHandlerMap[eventDetail.PositionID]
   685  	var f func(vugu.DOMEvent)
   686  	for _, h := range handlers {
   687  		if h.EventType == eventDetail.EventType && h.Capture == eventDetail.Capture {
   688  			f = h.Func
   689  			break
   690  		}
   691  	}
   692  
   693  	// make sure we found something, panic if not
   694  	if f == nil {
   695  		r.eventRWMU.Unlock()
   696  		panic(fmt.Errorf("Unable to find event handler for positionID=%q, eventType=%q, capture=%v",
   697  			eventDetail.PositionID, eventDetail.EventType, eventDetail.Capture))
   698  	}
   699  
   700  	// NOTE: For tinygo support we are not using defer here for now - it would probably be better to do so since
   701  	// the handler can panic.  However, Vugu program behavior after panicing from an event is currently
   702  	// undefined so whatever for now.  We'll have to make a decision later about whether or not Vugu
   703  	// programs should keep running after an event handler panics.  They do in JS after an exception,
   704  	// but... this is not JS.  Needs more thought.
   705  
   706  	// invoke handler
   707  	f(domEvent)
   708  
   709  	r.eventRWMU.Unlock()
   710  
   711  	// TODO: Also give this more thought: For now we just do a non-blocking push to the
   712  	// eventWaitCh, telling the render loop that a render is required, but if a bunch
   713  	// of them stack up we don't wait
   714  	r.sendEventWaitCh()
   715  
   716  }