runtimeError struct

runtimeError represents a captured runtime error.

Fields:

  • Message (string)
  • Stack (string)
  • Path (string)

addRuntimeError function

addRuntimeError appends a new runtime error and sets it as current.

Parameters:

  • e runtimeError

References:

Show/Hide Function Body
{
	if len(errList) >= maxRuntimeErrors {
		return
	}
	errList = append(errList, e)
	errIdx = len(errList) - 1
}

currentRuntimeError function

currentRuntimeError returns the active error.

Returns:

  • runtimeError
  • bool

References:

Show/Hide Function Body
{
	if errIdx < 0 || errIdx >= len(errList) {
		return runtimeError{}, false
	}
	return errList[errIdx], true
}

prevRuntimeError function

prevRuntimeError moves to the previous error if available.

Returns:

  • runtimeError
  • bool

References:

Show/Hide Function Body
{
	if errIdx > 0 {
		errIdx--
	}
	return currentRuntimeError()
}

nextRuntimeError function

nextRuntimeError moves to the next error if available.

Returns:

  • runtimeError
  • bool

References:

Show/Hide Function Body
{
	if errIdx < len(errList)-1 {
		errIdx++
	}
	return currentRuntimeError()
}

resetRuntimeErrors function

resetRuntimeErrors clears all tracked errors.

Show/Hide Function Body
{
	errList = nil
	errIdx = -1
}

runtimeErrorCount function

runtimeErrorCount returns the number of stored errors.

Returns:

  • int
Show/Hide Function Body
{ return len(errList) }

runtimeErrorIndex function

runtimeErrorIndex returns the current error index.

Returns:

  • int
Show/Hide Function Body
{ return errIdx }

init function

Show/Hide Function Body
{
	setupErrorListeners()

	doc := js.Document()
	if doc.Get("readyState").String() == "loading" {
		var readyFn js.Func
		readyFn = js.FuncOf(func(this js.Value, args []js.Value) any {
			readyFn.Release()
			setupErrorBox()
			return nil
		})
		doc.Call("addEventListener", "DOMContentLoaded", readyFn)
	} else {
		setupErrorBox()
	}
}

setupErrorBox function

Show/Hide Function Body
{
	doc := dom.Doc()
	style := doc.CreateElement("style")
	style.SetText(errorBoxCSS)
	doc.Head().AppendChild(style)

	box := composition.Div().Classes("rfw-errorbox", "hidden")
	boxEl := box.Element()
	boxEl.SetAttr("id", "rfwErrorBox")
	boxEl.SetAttr("role", "dialog")
	boxEl.SetAttr("aria-modal", "true")
	boxEl.SetAttr("aria-label", "Unhandled Runtime Error")
	boxEl.SetAttr("data-rfw-ignore", "")

	prevBtn := composition.Button().Classes("rfw-eb-iconbtn")
	prevBtnEl := prevBtn.Element()
	prevBtnEl.SetAttr("id", "ebPrev")
	prevBtnEl.SetAttr("title", "Previous")
	prevBtnEl.SetText("←")

	nextBtn := composition.Button().Classes("rfw-eb-iconbtn")
	nextBtnEl := nextBtn.Element()
	nextBtnEl.SetAttr("id", "ebNext")
	nextBtnEl.SetAttr("title", "Next")
	nextBtnEl.SetText("→")

	countSpan := composition.Span().Classes("rfw-eb-count")
	countEl := countSpan.Element()
	countEl.SetAttr("id", "ebCount")
	countEl.SetText("1 of 1 unhandled error")

	nav := composition.Div().Classes("rfw-eb-nav")
	nav.Element().AppendChild(prevBtnEl)
	nav.Element().AppendChild(nextBtnEl)
	nav.Element().AppendChild(countEl)

	closeBtn := composition.Button().Classes("rfw-eb-close")
	closeBtnEl := closeBtn.Element()
	closeBtnEl.SetAttr("id", "ebClose")
	closeBtnEl.SetAttr("title", "Close")
	closeBtnEl.SetText("✕")

	top := composition.Div().Classes("rfw-eb-top")
	top.Element().AppendChild(nav.Element())
	top.Element().AppendChild(closeBtnEl)

	title := composition.H(2).Classes("rfw-eb-title")
	titleEl := title.Element()
	titleEl.SetText("Unhandled Runtime Error")

	msg := composition.Div().Classes("rfw-eb-msg", "mono")
	msgEl := msg.Element()
	msgEl.SetAttr("id", "ebMsg")
	msgEl.SetText("Error: …")

	pathSpan := composition.Span().Classes("mono")
	pathEl := pathSpan.Element()
	pathEl.SetAttr("id", "ebFramePath")
	pathEl.SetText("-")

	frameHead := composition.Div().Classes("rfw-eb-framehead")
	frameHead.Element().AppendChild(pathEl)

	codeDiv := composition.Div().Classes("rfw-eb-code")
	codeEl := codeDiv.Element()
	codeEl.SetAttr("id", "ebCode")
	codeEl.SetAttr("aria-live", "polite")

	frame := composition.Div().Classes("rfw-eb-frame")
	frame.Element().AppendChild(frameHead.Element())
	frame.Element().AppendChild(codeEl)

	copyBtn := composition.Button().Classes("rfw-button")
	copyBtnEl := copyBtn.Element()
	copyBtnEl.SetAttr("id", "ebCopy")
	copyBtnEl.SetText("Copy error")

	spacer := composition.Span().Classes("rfw-spacer")
	spacerEl := spacer.Element()

	reloadBtn := composition.Button().Classes("rfw-button")
	reloadBtnEl := reloadBtn.Element()
	reloadBtnEl.SetAttr("id", "ebReload")
	reloadBtnEl.SetText("Reload")

	actions := composition.Div().Classes("rfw-eb-actions")
	actions.Element().AppendChild(copyBtnEl)
	actions.Element().AppendChild(spacerEl)
	actions.Element().AppendChild(reloadBtnEl)

	boxEl.AppendChild(top.Element())
	boxEl.AppendChild(titleEl)
	boxEl.AppendChild(msgEl)
	boxEl.AppendChild(frame.Element())
	boxEl.AppendChild(actions.Element())

	doc.Body().AppendChild(boxEl)

	render := func() {
		cur, ok := currentRuntimeError()
		if !ok {
			boxEl.AddClass("hidden")
			return
		}
		boxEl.RemoveClass("hidden")
		msgEl.SetText(cur.Message)
		codeEl.SetText(cur.Stack)
		pathEl.SetText(cur.Path)
		countEl.SetText(fmt.Sprintf("%d of %d unhandled error", runtimeErrorIndex()+1, runtimeErrorCount()))
		if runtimeErrorIndex() <= 0 {
			prevBtnEl.SetAttr("disabled", "true")
		} else {
			prevBtnEl.Call("removeAttribute", "disabled")
		}
		if runtimeErrorIndex() >= runtimeErrorCount()-1 {
			nextBtnEl.SetAttr("disabled", "true")
		} else {
			nextBtnEl.Call("removeAttribute", "disabled")
		}
	}

	prevBtnEl.On("click", func(dom.Event) { prevRuntimeError(); render() })
	nextBtnEl.On("click", func(dom.Event) { nextRuntimeError(); render() })
	closeBtnEl.On("click", func(dom.Event) { resetRuntimeErrors(); render() })
	copyBtnEl.On("click", func(dom.Event) {
		cur, ok := currentRuntimeError()
		if ok {
			js.Window().Get("navigator").Get("clipboard").Call("writeText", cur.Message+"\n"+cur.Stack)
		}
	})
	reloadBtnEl.On("click", func(dom.Event) { js.Location().Call("reload") })

	renderFn = render
	render()
}

setupErrorListeners function

Show/Hide Function Body
{
	errEvtFn = js.FuncOf(func(this js.Value, args []js.Value) any {
		e := args[0]
		msg := e.Get("message").String()
		stack := ""
		if v := e.Get("error"); v.Type() == js.TypeObject {
			stack = v.Get("stack").String()
		}
		path := parsePath(stack)
		addRuntimeError(runtimeError{Message: msg, Stack: stack, Path: path})
		if renderFn != nil {
			renderFn()
		}
		return nil
	})
	rejEvtFn = js.FuncOf(func(this js.Value, args []js.Value) any {
		e := args[0]
		reason := e.Get("reason")
		msg := reason.Get("message").String()
		stack := reason.Get("stack").String()
		path := parsePath(stack)
		addRuntimeError(runtimeError{Message: msg, Stack: stack, Path: path})
		if renderFn != nil {
			renderFn()
		}
		return nil
	})
	js.Window().Call("addEventListener", "error", errEvtFn)
	js.Window().Call("addEventListener", "unhandledrejection", rejEvtFn)
}

parsePath function

Parameters:

  • stack string

Returns:

  • string
Show/Hide Function Body
{
	lines := strings.Split(stack, "\n")
	for _, l := range lines {
		if strings.Contains(l, ".go") {
			l = strings.TrimSpace(l)
			return l
		}
	}
	return ""
}

storeBinding struct

Fields:

  • Module (string) - json:"module"
  • Name (string) - json:"name"
  • Keys ([]string) - json:"keys,omitempty"

recordStoreBinding function

Parameters:

  • componentID string
  • module string
  • store string
  • key string
Show/Hide Function Body
{
	if componentID == "" || module == "" || store == "" || key == "" {
		return
	}
	storeUsageMu.Lock()
	defer storeUsageMu.Unlock()
	moduleMap, ok := storeUsage[componentID]
	if !ok {
		moduleMap = make(map[string]map[string]map[string]struct{})
		storeUsage[componentID] = moduleMap
	}
	storeMap, ok := moduleMap[module]
	if !ok {
		storeMap = make(map[string]map[string]struct{})
		moduleMap[module] = storeMap
	}
	keySet, ok := storeMap[store]
	if !ok {
		keySet = make(map[string]struct{})
		storeMap[store] = keySet
	}
	keySet[key] = struct{}{}
}

dropStoreBindings function

Parameters:

  • componentID string
Show/Hide Function Body
{
	if componentID == "" {
		return
	}
	storeUsageMu.Lock()
	delete(storeUsage, componentID)
	storeUsageMu.Unlock()
}

snapshotStoreBindings function

Parameters:

  • componentID string

Returns:

  • []storeBinding
Show/Hide Function Body
{
	storeUsageMu.RLock()
	moduleMap := storeUsage[componentID]
	storeUsageMu.RUnlock()
	if len(moduleMap) == 0 {
		return nil
	}
	modules := make([]string, 0, len(moduleMap))
	for module := range moduleMap {
		modules = append(modules, module)
	}
	sort.Strings(modules)
	bindings := make([]storeBinding, 0)
	for _, module := range modules {
		storeMap := moduleMap[module]
		storeNames := make([]string, 0, len(storeMap))
		for storeName := range storeMap {
			storeNames = append(storeNames, storeName)
		}
		sort.Strings(storeNames)
		for _, storeName := range storeNames {
			keySet := storeMap[storeName]
			keys := make([]string, 0, len(keySet))
			for key := range keySet {
				keys = append(keys, key)
			}
			sort.Strings(keys)
			bindings = append(bindings, storeBinding{Module: module, Name: storeName, Keys: keys})
		}
	}
	return bindings
}

resetStoreUsage function

Show/Hide Function Body
{
	storeUsageMu.Lock()
	storeUsage = map[string]map[string]map[string]map[string]struct{}{}
	storeUsageMu.Unlock()
}

lifecycleEvent struct

Fields:

  • Kind (string)
  • At (time.Time)

appendLifecycle function

Parameters:

  • id string
  • kind string
  • at time.Time
Show/Hide Function Body
{
	if id == "" || kind == "" {
		return
	}
	lifecycleMu.Lock()
	defer lifecycleMu.Unlock()
	entry := lifecycleEvent{Kind: kind, At: at}
	list := append(lifecycleByComponent[id], entry)
	if len(list) > lifecycleLimit {
		list = append([]lifecycleEvent(nil), list[len(list)-lifecycleLimit:]...)
	}
	lifecycleByComponent[id] = list
}

dropLifecycle function

Parameters:

  • id string
Show/Hide Function Body
{
	if id == "" {
		return
	}
	lifecycleMu.Lock()
	delete(lifecycleByComponent, id)
	lifecycleMu.Unlock()
}

snapshotLifecycle function

Parameters:

  • id string

Returns:

  • []lifecycleEvent
Show/Hide Function Body
{
	lifecycleMu.RLock()
	list := lifecycleByComponent[id]
	lifecycleMu.RUnlock()
	if len(list) == 0 {
		return nil
	}
	out := make([]lifecycleEvent, len(list))
	copy(out, list)
	sort.Slice(out, func(i, j int) bool { return out[i].At.Before(out[j].At) })
	return out
}

resetLifecycles function

Show/Hide Function Body
{
	lifecycleMu.Lock()
	lifecycleByComponent = map[string][]lifecycleEvent{}
	lifecycleMu.Unlock()
}

node struct

Fields:

  • ID (int) - json:"id"
  • Kind (string) - json:"kind"
  • Name (string) - json:"name"
  • Time (float64) - json:"time"
  • Average (float64) - json:"average,omitempty"
  • Total (float64) - json:"total,omitempty"
  • Path (string) - json:"path"
  • Owner (string) - json:"owner,omitempty"
  • Host (string) - json:"hostComponent,omitempty"
  • Updates (int) - json:"updates,omitempty"
  • Props (map[string]any) - json:"props,omitempty"
  • Slots (map[string]any) - json:"slots,omitempty"
  • Signals (map[string]any) - json:"signals,omitempty"
  • StoreBindings ([]storeBinding) - json:"storeBindings,omitempty"
  • Store (*storeSnapshot) - json:"store,omitempty"
  • Children ([]*node) - json:"children,omitempty"
  • Timeline ([]timelineEntry) - json:"timeline,omitempty"

timelineEntry struct

Fields:

  • Kind (string) - json:"kind"
  • At (int64) - json:"at"
  • Duration (float64) - json:"duration,omitempty"

storeSnapshot struct

Fields:

  • Module (string) - json:"module"
  • Name (string) - json:"name"
  • State (map[string]any) - json:"state,omitempty"

addComponent function

Parameters:

  • id string
  • kind string
  • name string
  • parentID string

Returns:

  • *node
Show/Hide Function Body
{
	mu.Lock()
	defer mu.Unlock()
	n := &node{ID: nextID, Kind: kind, Name: name, Path: "/" + name}
	nextID++
	nodes[id] = n
	if parentID != "" {
		if p, ok := nodes[parentID]; ok {
			n.Path = p.Path + "/" + name
			n.Owner = p.Name
			p.Children = append(p.Children, n)
			return n
		}
	}
	roots = append(roots, n)
	return n
}

removeComponent function

Parameters:

  • id string
Show/Hide Function Body
{
	mu.Lock()
	defer mu.Unlock()
	n, ok := nodes[id]
	if !ok {
		return
	}
	for _, p := range nodes {
		for i, ch := range p.Children {
			if ch == n {
				p.Children = append(p.Children[:i], p.Children[i+1:]...)
				delete(nodes, id)
				dropLifecycle(id)
				return
			}
		}
	}
	for i, r := range roots {
		if r == n {
			roots = append(roots[:i], roots[i+1:]...)
			break
		}
	}
	delete(nodes, id)
	dropLifecycle(id)
}

resetTree function

Show/Hide Function Body
{
	mu.Lock()
	roots = nil
	nodes = map[string]*node{}
	nextID = 0
	mu.Unlock()
}

treeJSON function

Returns:

  • string
Show/Hide Function Body
{
	mu.RLock()
	defer mu.RUnlock()
	b, _ := json.Marshal(roots)
	return string(b)
}

storeInspector interface

Methods:

Snapshot


Returns:
  • map[string]any

Module


Returns:
  • string

Name


Returns:
  • string

captureTree function

Parameters:

  • c core.Component
Show/Hide Function Body
{
	resetTree()
	walk(c, "")
}

walk function

Parameters:

  • c core.Component
  • parentID string
Show/Hide Function Body
{
	if c == nil {
		return
	}
	id := c.GetID()
	kind := componentKind(c)
	name := c.GetName()
	n := addComponent(id, kind, name, parentID)
	populateMetadata(n, c)
	for _, child := range extractDependencies(c) {
		walk(child, id)
	}
}

componentKind function

Parameters:

  • c core.Component

Returns:

  • string
Show/Hide Function Body
{
	v := reflect.ValueOf(c)
	if !v.IsValid() {
		return ""
	}
	t := v.Type()
	if t.Kind() == reflect.Pointer {
		t = t.Elem()
	}
	if t.Name() != "" {
		return t.Name()
	}
	return t.String()
}

extractDependencies function

Parameters:

  • c core.Component

Returns:

  • []core.Component
Show/Hide Function Body
{
	v := reflect.ValueOf(c)
	if !v.IsValid() {
		return nil
	}
	v = reflect.Indirect(v)
	if !v.IsValid() {
		return nil
	}
	if deps := mapOfComponents(v.FieldByName("Dependencies")); len(deps) > 0 {
		return deps
	}
	if hc := unwrapHTMLComponentValue(c); hc.IsValid() {
		if deps := mapOfComponents(reflect.Indirect(hc).FieldByName("Dependencies")); len(deps) > 0 {
			return deps
		}
	}
	return nil
}

mapOfComponents function

Parameters:

  • field reflect.Value

Returns:

  • []core.Component
Show/Hide Function Body
{
	if !field.IsValid() || field.IsNil() {
		return nil
	}
	if !field.CanInterface() {
		return nil
	}
	if deps, ok := field.Interface().(map[string]core.Component); ok {
		list := make([]core.Component, 0, len(deps))
		for _, child := range deps {
			if child != nil {
				list = append(list, child)
			}
		}
		sort.Slice(list, func(i, j int) bool { return list[i].GetName() < list[j].GetName() })
		return list
	}
	return nil
}

populateMetadata function

Parameters:

  • n *node
  • c core.Component
Show/Hide Function Body
{
	if n == nil || c == nil {
		return
	}
	v := reflect.ValueOf(c)
	if v.Kind() == reflect.Pointer && !v.IsNil() {
		v = v.Elem()
	}
	html := unwrapHTMLComponentValue(c)
	if html.IsValid() {
		assignMaps(n, html)
		assignStore(n, html)
		if host := extractString(html, "HostComponent"); host != "" {
			n.Host = host
		}
	}
	assignMaps(n, v)
	assignStore(n, v)
	if n.Host == "" {
		if host := extractString(v, "HostComponent"); host != "" {
			n.Host = host
		}
	}
	if updates := extractInt(v, "Updates"); updates > 0 {
		n.Updates = updates
	}
	if stats, ok := statsFromComponent(c); ok {
		applyStats(n, c.GetID(), stats)
	}
	if owner := extractString(v, "Owner"); owner != "" {
		n.Owner = owner
	}
	if signals := dom.SnapshotComponentSignals(c.GetID()); len(signals) > 0 {
		sanitized := make(map[string]any, len(signals))
		keys := make([]string, 0, len(signals))
		for k := range signals {
			keys = append(keys, k)
		}
		sort.Strings(keys)
		for _, key := range keys {
			sanitized[key] = sanitizeValue(signals[key])
		}
		n.Signals = sanitized
	} else if n.Signals == nil {
		if m := extractMap(v, "Signals"); len(m) > 0 {
			n.Signals = m
		}
	}
	applyStoreBindings(n, c.GetID())
}

statsProvider interface

Methods:

Stats


Returns:
  • core.ComponentStats

statsFromComponent function

Parameters:

  • c core.Component

Returns:

  • core.ComponentStats
  • bool
Show/Hide Function Body
{
	if c == nil {
		return core.ComponentStats{}, false
	}
	if provider, ok := any(c).(statsProvider); ok {
		return provider.Stats(), true
	}
	if html := unwrapHTMLComponentValue(c); html.IsValid() {
		return statsFromValue(html)
	}
	return core.ComponentStats{}, false
}

statsFromValue function

Parameters:

  • v reflect.Value

Returns:

  • core.ComponentStats
  • bool
Show/Hide Function Body
{
	if !v.IsValid() {
		return core.ComponentStats{}, false
	}
	if v.Kind() == reflect.Pointer && v.IsNil() {
		return core.ComponentStats{}, false
	}
	if v.Kind() != reflect.Pointer {
		if v.CanAddr() {
			v = v.Addr()
		} else {
			return core.ComponentStats{}, false
		}
	}
	if !v.CanInterface() {
		return core.ComponentStats{}, false
	}
	if provider, ok := v.Interface().(statsProvider); ok {
		return provider.Stats(), true
	}
	return core.ComponentStats{}, false
}

applyStats function

Parameters:

  • n *node
  • id string
  • stats core.ComponentStats
Show/Hide Function Body
{
	if n == nil {
		return
	}
	if stats.LastRender > 0 {
		n.Time = durationToMillis(stats.LastRender)
	}
	if stats.AverageRender > 0 {
		n.Average = durationToMillis(stats.AverageRender)
	}
	if stats.TotalRender > 0 {
		n.Total = durationToMillis(stats.TotalRender)
	}
	if stats.RenderCount > n.Updates {
		n.Updates = stats.RenderCount
	}
	timeline := combineTimelines(stats.Timeline, snapshotLifecycle(id))
	if len(timeline) > 0 {
		n.Timeline = timeline
	}
}

applyStoreBindings function

Parameters:

  • n *node
  • componentID string
Show/Hide Function Body
{
	if n == nil || componentID == "" {
		return
	}
	if bindings := snapshotStoreBindings(componentID); len(bindings) > 0 {
		n.StoreBindings = bindings
	}
}

durationToMillis function

Parameters:

  • d time.Duration

Returns:

  • float64
Show/Hide Function Body
{
	if d <= 0 {
		return 0
	}
	return float64(d) / float64(time.Millisecond)
}

combineTimelines function

Parameters:

  • render []core.ComponentTimelineEntry
  • lifecycle []lifecycleEvent

Returns:

  • []timelineEntry
Show/Hide Function Body
{
	total := len(render) + len(lifecycle)
	if total == 0 {
		return nil
	}
	merged := make([]timelineEntry, 0, total)
	for _, ev := range render {
		if ev.Kind == "" {
			continue
		}
		at := ev.Timestamp.UnixNano() / int64(time.Millisecond)
		merged = append(merged, timelineEntry{
			Kind:     ev.Kind,
			At:       at,
			Duration: durationToMillis(ev.Duration),
		})
	}
	for _, ev := range lifecycle {
		if ev.Kind == "" {
			continue
		}
		merged = append(merged, timelineEntry{
			Kind: ev.Kind,
			At:   ev.At.UnixNano() / int64(time.Millisecond),
		})
	}
	if len(merged) == 0 {
		return nil
	}
	sort.SliceStable(merged, func(i, j int) bool {
		if merged[i].At == merged[j].At {
			if merged[i].Duration == merged[j].Duration {
				return merged[i].Kind < merged[j].Kind
			}
			return merged[i].Duration < merged[j].Duration
		}
		return merged[i].At < merged[j].At
	})
	base := merged[0].At
	for i := range merged {
		merged[i].At -= base
		if merged[i].At < 0 {
			merged[i].At = 0
		}
	}
	return merged
}

assignMaps function

Parameters:

  • n *node
  • v reflect.Value
Show/Hide Function Body
{
	if !v.IsValid() {
		return
	}
	if n.Props == nil {
		if props := extractMap(v, "Props"); len(props) > 0 {
			n.Props = props
		}
	}
	if n.Slots == nil {
		if slots := extractMap(v, "Slots"); len(slots) > 0 {
			n.Slots = slots
		}
	}
}

assignStore function

Parameters:

  • n *node
  • v reflect.Value
Show/Hide Function Body
{
	if !v.IsValid() || n.Store != nil {
		return
	}
	if snap := extractStore(v, "Store"); snap != nil {
		n.Store = snap
	}
}

extractStore function

Parameters:

  • v reflect.Value
  • field string

Returns:

  • *storeSnapshot
Show/Hide Function Body
{
	f := fieldByName(v, field)
	if !f.IsValid() || !f.CanInterface() {
		return nil
	}
	if isNilable(f.Kind()) && f.IsNil() {
		return nil
	}
	if inspector, ok := f.Interface().(storeInspector); ok {
		state := sanitizeMap(inspector.Snapshot())
		return &storeSnapshot{
			Module: inspector.Module(),
			Name:   inspector.Name(),
			State:  state,
		}
	}
	return nil
}

extractMap function

Parameters:

  • v reflect.Value
  • field string

Returns:

  • map[string]any
Show/Hide Function Body
{
	f := fieldByName(v, field)
	if !f.IsValid() {
		return nil
	}
	if isNilable(f.Kind()) && f.IsNil() {
		return nil
	}
	if !f.CanInterface() {
		return nil
	}
	switch val := f.Interface().(type) {
	case map[string]any:
		return sanitizeMap(val)
	default:
		if f.Kind() == reflect.Map {
			iter := f.MapRange()
			tmp := make(map[string]any, f.Len())
			for iter.Next() {
				key := keyToString(iter.Key())
				tmp[key] = sanitizeValue(iter.Value().Interface())
			}
			return sanitizeMap(tmp)
		}
	}
	return nil
}

extractString function

Parameters:

  • v reflect.Value
  • field string

Returns:

  • string
Show/Hide Function Body
{
	f := fieldByName(v, field)
	if !f.IsValid() || !f.CanInterface() {
		return ""
	}
	if s, ok := f.Interface().(string); ok {
		return s
	}
	if f.Kind() == reflect.String {
		return f.String()
	}
	return ""
}

extractInt function

Parameters:

  • v reflect.Value
  • field string

Returns:

  • int
Show/Hide Function Body
{
	f := fieldByName(v, field)
	if !f.IsValid() || !f.CanInterface() {
		return 0
	}
	if i, ok := f.Interface().(int); ok {
		return i
	}
	if f.Kind() == reflect.Int || f.Kind() == reflect.Int64 || f.Kind() == reflect.Int32 {
		return int(f.Int())
	}
	return 0
}

fieldByName function

Parameters:

  • v reflect.Value
  • name string

Returns:

  • reflect.Value
Show/Hide Function Body
{
	if !v.IsValid() {
		return reflect.Value{}
	}
	if v.Kind() == reflect.Pointer {
		if v.IsNil() {
			return reflect.Value{}
		}
		v = v.Elem()
	}
	if !v.IsValid() {
		return reflect.Value{}
	}
	return v.FieldByName(name)
}

unwrapHTMLComponentValue function

Parameters:

  • c core.Component

Returns:

  • reflect.Value
Show/Hide Function Body
{
	val := reflect.ValueOf(c)
	if !val.IsValid() {
		return reflect.Value{}
	}
	if val.Type() == htmlComponentPtrType {
		return val
	}
	if val.Kind() == reflect.Pointer && !val.IsNil() {
		if val.Elem().Type() == htmlComponentType {
			return val
		}
		val = val.Elem()
	}
	if !val.IsValid() {
		return reflect.Value{}
	}
	if val.Type() == htmlComponentType {
		if val.CanAddr() {
			return val.Addr()
		}
		return reflect.Value{}
	}
	field := val.FieldByName("HTMLComponent")
	if !field.IsValid() {
		return reflect.Value{}
	}
	if field.Kind() == reflect.Pointer {
		if field.IsNil() {
			return reflect.Value{}
		}
		return field
	}
	if field.CanAddr() {
		return field.Addr()
	}
	return reflect.Value{}
}

sanitizeMap function

Parameters:

  • in map[string]any

Returns:

  • map[string]any
Show/Hide Function Body
{
	if len(in) == 0 {
		return nil
	}
	out := make(map[string]any, len(in))
	keys := make([]string, 0, len(in))
	for k := range in {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
		out[k] = sanitizeValue(in[k])
	}
	return out
}

sanitizeSlice function

Parameters:

  • in []any

Returns:

  • []any
Show/Hide Function Body
{
	if len(in) == 0 {
		return nil
	}
	out := make([]any, len(in))
	for i, v := range in {
		out[i] = sanitizeValue(v)
	}
	return out
}

sanitizeValue function

Parameters:

  • v any

Returns:

  • any
Show/Hide Function Body
{
	switch val := v.(type) {
	case nil:
		return nil
	case string, bool:
		return val
	case json.Number:
		return val.String()
	case fmt.Stringer:
		return val.String()
	case json.Marshaler:
		if b, err := val.MarshalJSON(); err == nil {
			var out any
			if err := json.Unmarshal(b, &out); err == nil {
				return out
			}
			return string(b)
		}
		return fmt.Sprintf("%v", v)
	case []byte:
		return string(val)
	case map[string]any:
		return sanitizeMap(val)
	case []any:
		return sanitizeSlice(val)
	}
	if reader, ok := v.(interface{ Read() any }); ok {
		return sanitizeValue(reader.Read())
	}
	rv := reflect.ValueOf(v)
	if !rv.IsValid() {
		return nil
	}
	switch rv.Kind() {
	case reflect.Pointer:
		if rv.IsNil() {
			return nil
		}
		return sanitizeValue(rv.Elem().Interface())
	case reflect.Slice, reflect.Array:
		arr := make([]any, rv.Len())
		for i := 0; i < rv.Len(); i++ {
			arr[i] = sanitizeValue(rv.Index(i).Interface())
		}
		return arr
	case reflect.Map:
		result := make(map[string]any, rv.Len())
		iter := rv.MapRange()
		for iter.Next() {
			key := keyToString(iter.Key())
			result[key] = sanitizeValue(iter.Value().Interface())
		}
		return sanitizeMap(result)
	case reflect.Struct:
		return fmt.Sprintf("%v", v)
	case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8:
		return rv.Int()
	case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8:
		return rv.Uint()
	case reflect.Float32, reflect.Float64:
		return rv.Float()
	}
	return fmt.Sprintf("%v", v)
}

keyToString function

Parameters:

  • v reflect.Value

Returns:

  • string
Show/Hide Function Body
{
	if !v.IsValid() {
		return ""
	}
	if v.Kind() == reflect.String {
		return v.String()
	}
	if v.CanInterface() {
		return fmt.Sprintf("%v", v.Interface())
	}
	return fmt.Sprintf("%v", v)
}

isNilable function

Parameters:

  • kind reflect.Kind

Returns:

  • bool
Show/Hide Function Body
{
	switch kind {
	case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
		return true
	default:
		return false
	}
}

mockComponent struct

Fields:

  • name (string)
  • id (string)
  • Dependencies (map[string]core.Component)
  • Props (map[string]any)
  • Slots (map[string]any)
  • Signals (map[string]any)
  • Store (storeInspector)
  • HostComponent (string)
  • Updates (int)
  • stats (core.ComponentStats)

Implements:

  • statsProvider from devtools

Methods:

Render


Returns:
  • string

Show/Hide Method Body
{ return "" }

GetName


Returns:
  • string

Show/Hide Method Body
{ return m.name }

GetID


Returns:
  • string

Show/Hide Method Body
{ return m.id }

Stats


Returns:
  • core.ComponentStats

Show/Hide Method Body
{
	return m.stats
}

TestCaptureTree function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	resetLifecycles()
	child := &mockComponent{name: "Child", id: "child"}
	root := &mockComponent{name: "Root", id: "root", Dependencies: map[string]core.Component{"child": child}}

	captureTree(root)

	if len(roots) != 1 || len(roots[0].Children) != 1 {
		t.Fatalf("tree not built correctly: %+v", roots)
	}
}

TestTreeJSON function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	resetLifecycles()
	root := &mockComponent{name: "A", id: "a"}
	captureTree(root)
	js := treeJSON()
	if js == "" || js[0] != '[' {
		t.Fatalf("unexpected json: %s", js)
	}
}

TestCaptureTreeNested function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	resetLifecycles()
	grand := &mockComponent{name: "Grand", id: "grand"}
	child := &mockComponent{name: "Child", id: "child", Dependencies: map[string]core.Component{"grand": grand}}
	root := &mockComponent{name: "Root", id: "root", Dependencies: map[string]core.Component{"child": child}}

	captureTree(root)

	if len(roots) != 1 || len(roots[0].Children) != 1 || len(roots[0].Children[0].Children) != 1 {
		t.Fatalf("tree not built correctly: %+v", roots)
	}
}

fakeStore struct

Fields:

  • module (string)
  • name (string)
  • state (map[string]any)

Implements:

  • storeInspector from devtools

Methods:

Snapshot


Returns:
  • map[string]any

Show/Hide Method Body
{
	dup := make(map[string]any, len(s.state))
	for k, v := range s.state {
		dup[k] = v
	}
	return dup
}

Module


Returns:
  • string

Show/Hide Method Body
{ return s.module }

Name


Returns:
  • string

Show/Hide Method Body
{ return s.name }

TestCaptureTreeMetadata function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	resetLifecycles()
	resetStoreUsage()
	t.Cleanup(resetStoreUsage)
	child := &mockComponent{name: "Child", id: "child", Updates: 2}
	root := &mockComponent{
		name: "Root",
		id:   "root",
		Dependencies: map[string]core.Component{
			"child": child,
		},
		Props: map[string]any{"title": "hello", "count": 3},
		Slots: map[string]any{"header": "value"},
		Signals: map[string]any{
			"selected": "item",
		},
		Store: &fakeStore{
			module: "app",
			name:   "main",
			state: map[string]any{
				"count": 7,
			},
		},
		HostComponent: "Widget",
		Updates:       5,
	}

	recordStoreBinding("root", "app", "main", "count")
	recordStoreBinding("root", "app", "main", "title")
	recordStoreBinding("child", "app", "child", "ready")

	captureTree(root)

	if len(roots) != 1 {
		t.Fatalf("expected single root, got %+v", roots)
	}
	gotRoot := roots[0]
	if gotRoot.Props["title"] != "hello" {
		t.Fatalf("expected props copied, got %+v", gotRoot.Props)
	}
	if gotRoot.Slots["header"] != "value" {
		t.Fatalf("expected slots copied, got %+v", gotRoot.Slots)
	}
	if gotRoot.Signals["selected"] != "item" {
		t.Fatalf("expected signals copied, got %+v", gotRoot.Signals)
	}
	if gotRoot.Host != "Widget" {
		t.Fatalf("expected host set, got %q", gotRoot.Host)
	}
	if gotRoot.Store == nil || gotRoot.Store.Module != "app" || gotRoot.Store.Name != "main" {
		t.Fatalf("unexpected store snapshot: %+v", gotRoot.Store)
	}
	if gotRoot.Store.State["count"] != int64(7) && gotRoot.Store.State["count"] != float64(7) {
		t.Fatalf("expected store state value, got %+v", gotRoot.Store.State["count"])
	}
	if len(gotRoot.Children) != 1 {
		t.Fatalf("expected child node, got %+v", gotRoot.Children)
	}
	childNode := gotRoot.Children[0]
	if childNode.Owner != "Root" {
		t.Fatalf("expected owner to be root, got %q", childNode.Owner)
	}
	if childNode.Updates != 2 {
		t.Fatalf("expected child updates, got %d", childNode.Updates)
	}
	if len(gotRoot.StoreBindings) != 1 {
		t.Fatalf("expected single store binding for root, got %+v", gotRoot.StoreBindings)
	}
	rootBinding := gotRoot.StoreBindings[0]
	if rootBinding.Module != "app" || rootBinding.Name != "main" {
		t.Fatalf("unexpected root binding metadata: %+v", rootBinding)
	}
	if len(rootBinding.Keys) != 2 || rootBinding.Keys[0] != "count" || rootBinding.Keys[1] != "title" {
		t.Fatalf("unexpected root binding keys: %+v", rootBinding.Keys)
	}
	if len(childNode.StoreBindings) != 1 {
		t.Fatalf("expected child binding, got %+v", childNode.StoreBindings)
	}
	if childNode.StoreBindings[0].Keys[0] != "ready" {
		t.Fatalf("unexpected child binding keys: %+v", childNode.StoreBindings[0].Keys)
	}
}

TestStoreBindingSnapshot function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	resetStoreUsage()
	recordStoreBinding("cmp", "app", "main", "count")
	recordStoreBinding("cmp", "app", "main", "count")
	recordStoreBinding("cmp", "app", "main", "title")
	recordStoreBinding("cmp", "admin", "users", "list")
	got := snapshotStoreBindings("cmp")
	if len(got) != 2 {
		t.Fatalf("expected two store bindings, got %+v", got)
	}
	if got[0].Module != "admin" || got[0].Name != "users" {
		t.Fatalf("unexpected ordering: %+v", got)
	}
	if len(got[0].Keys) != 1 || got[0].Keys[0] != "list" {
		t.Fatalf("unexpected admin keys: %+v", got[0].Keys)
	}
	if len(got[1].Keys) != 2 || got[1].Keys[0] != "count" || got[1].Keys[1] != "title" {
		t.Fatalf("unexpected app keys: %+v", got[1].Keys)
	}
	dropStoreBindings("cmp")
	if bindings := snapshotStoreBindings("cmp"); bindings != nil {
		t.Fatalf("expected bindings cleared, got %+v", bindings)
	}
}

TestCaptureTreeStatsAndTimeline function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	resetLifecycles()
	now := time.Now()
	appendLifecycle("root", "mount", now)
	appendLifecycle("root", "unmount", now.Add(15*time.Millisecond))
	root := &mockComponent{
		name: "Root",
		id:   "root",
		stats: core.ComponentStats{
			RenderCount:   3,
			TotalRender:   30 * time.Millisecond,
			LastRender:    12 * time.Millisecond,
			AverageRender: 10 * time.Millisecond,
			Timeline: []core.ComponentTimelineEntry{
				{Kind: "render", Timestamp: now.Add(5 * time.Millisecond), Duration: 8 * time.Millisecond},
			},
		},
	}

	captureTree(root)

	if len(roots) != 1 {
		t.Fatalf("expected single root, got %+v", roots)
	}
	got := roots[0]
	if got.Updates != 3 {
		t.Fatalf("expected render count propagated, got %d", got.Updates)
	}
	if math.Abs(got.Average-10) > 0.01 {
		t.Fatalf("expected average 10ms, got %.2f", got.Average)
	}
	if math.Abs(got.Time-12) > 0.01 {
		t.Fatalf("expected last render 12ms, got %.2f", got.Time)
	}
	if math.Abs(got.Total-30) > 0.01 {
		t.Fatalf("expected total 30ms, got %.2f", got.Total)
	}
	if len(got.Timeline) != 3 {
		t.Fatalf("expected merged timeline, got %+v", got.Timeline)
	}
	if got.Timeline[0].Kind != "mount" {
		t.Fatalf("expected mount first, got %+v", got.Timeline)
	}
	if got.Timeline[len(got.Timeline)-1].Kind != "unmount" {
		t.Fatalf("expected unmount last, got %+v", got.Timeline)
	}
}

plugin struct

Methods:

Build


Parameters:
  • json.RawMessage

Returns:
  • error

Show/Hide Method Body
{ return nil }

Install


Parameters:
  • a *core.App

Show/Hide Method Body
{
	resetStoreUsage()
	dom.StoreBindingHook = recordStoreBinding
	a.RegisterLifecycle(func(c core.Component) {
		appendLifecycle(c.GetID(), "mount", time.Now())
		if root == nil {
			root = c
		}
		if root != nil {
			captureTree(root)
			if fn := js.Get("RFW_DEVTOOLS_REFRESH"); fn.Type() == js.TypeFunction {
				fn.Invoke()
			}
		}
	}, func(c core.Component) {
		appendLifecycle(c.GetID(), "unmount", time.Now())
		if root == c {
			resetTree()
			root = nil
		} else if root != nil {
			captureTree(root)
			if fn := js.Get("RFW_DEVTOOLS_REFRESH"); fn.Type() == js.TypeFunction {
				fn.Invoke()
			}
		}
		dropLifecycle(c.GetID())
		dropStoreBindings(c.GetID())
	})
	a.RegisterRouter(func(_ string) {
		if root != nil {
			captureTree(root)
			if fn := js.Get("RFW_DEVTOOLS_REFRESH"); fn.Type() == js.TypeFunction {
				fn.Invoke()
			}
		}
	})
	a.RegisterStore(func(_, _, _ string, _ any) {
		if fn := js.Get("RFW_DEVTOOLS_REFRESH_STORES"); fn.Type() == js.TypeFunction {
			fn.Invoke()
		}
	})
	state.SignalHook = func(int, any) {
		if fn := js.Get("RFW_DEVTOOLS_REFRESH_SIGNALS"); fn.Type() == js.TypeFunction {
			fn.Invoke()
		}
	}
	http.RegisterHTTPHook(func(start bool, url string, status int, d time.Duration) {
		if obj := js.Get("RFW_DEVTOOLS"); obj.Type() == js.TypeObject {
			if fn := obj.Get("network"); fn.Type() == js.TypeFunction {
				fn.Invoke(start, url, status, d.Milliseconds())
			}
		}
	})
	if fn := js.Get("RFW_DEVTOOLS_REFRESH_ROUTES"); fn.Type() == js.TypeFunction {
		fn.Invoke()
	}
}

init function

Show/Hide Function Body
{
	core.RegisterPlugin(plugin{})
	treeFn = js.FuncOf(func(this js.Value, args []js.Value) any {
		if root != nil {
			captureTree(root)
		}
		return treeJSON()
	})
	storeFn = js.FuncOf(func(this js.Value, args []js.Value) any {
		b, _ := json.Marshal(state.GlobalStoreManager.Snapshot())
		return string(b)
	})
	signalFn = js.FuncOf(func(this js.Value, args []js.Value) any {
		b, _ := json.Marshal(state.SnapshotSignals())
		return string(b)
	})
	routesFn = js.FuncOf(func(this js.Value, args []js.Value) any {
		b, _ := json.Marshal(router.RegisteredRoutes())
		return string(b)
	})
	js.Set("RFW_DEVTOOLS_TREE", treeFn)
	js.Set("RFW_DEVTOOLS_STORES", storeFn)
	js.Set("RFW_DEVTOOLS_SIGNALS", signalFn)
	js.Set("RFW_DEVTOOLS_ROUTES", routesFn)
}

fmt import

Import example:

import "fmt"

strings import

Import example:

import "strings"

github.com/rfwlab/rfw/v1/composition import

Import example:

import "github.com/rfwlab/rfw/v1/composition"

github.com/rfwlab/rfw/v1/dom import

Import example:

import "github.com/rfwlab/rfw/v1/dom"

github.com/rfwlab/rfw/v1/js import

Import example:

import "github.com/rfwlab/rfw/v1/js"

Imported as:

js

sort import

Import example:

import "sort"

sync import

Import example:

import "sync"

sort import

Import example:

import "sort"

sync import

Import example:

import "sync"

time import

Import example:

import "time"

encoding/json import

Import example:

import "encoding/json"

sync import

Import example:

import "sync"

encoding/json import

Import example:

import "encoding/json"

fmt import

Import example:

import "fmt"

reflect import

Import example:

import "reflect"

sort import

Import example:

import "sort"

time import

Import example:

import "time"

github.com/rfwlab/rfw/v1/core import

Import example:

import "github.com/rfwlab/rfw/v1/core"

github.com/rfwlab/rfw/v1/dom import

Import example:

import "github.com/rfwlab/rfw/v1/dom"

math import

Import example:

import "math"

testing import

Import example:

import "testing"

time import

Import example:

import "time"

github.com/rfwlab/rfw/v1/core import

Import example:

import "github.com/rfwlab/rfw/v1/core"

encoding/json import

Import example:

import "encoding/json"

github.com/rfwlab/rfw/v1/core import

Import example:

import "github.com/rfwlab/rfw/v1/core"

github.com/rfwlab/rfw/v1/dom import

Import example:

import "github.com/rfwlab/rfw/v1/dom"

github.com/rfwlab/rfw/v1/http import

Import example:

import "github.com/rfwlab/rfw/v1/http"

github.com/rfwlab/rfw/v1/js import

Import example:

import "github.com/rfwlab/rfw/v1/js"

github.com/rfwlab/rfw/v1/router import

Import example:

import "github.com/rfwlab/rfw/v1/router"

github.com/rfwlab/rfw/v1/state import

Import example:

import "github.com/rfwlab/rfw/v1/state"

time import

Import example:

import "time"