runtimeError represents a captured runtime error.
addRuntimeError appends a new runtime error and sets it as current.
{
if len(errList) >= maxRuntimeErrors {
return
}
errList = append(errList, e)
errIdx = len(errList) - 1
}
currentRuntimeError returns the active error.
{
if errIdx < 0 || errIdx >= len(errList) {
return runtimeError{}, false
}
return errList[errIdx], true
}
prevRuntimeError moves to the previous error if available.
{
if errIdx > 0 {
errIdx--
}
return currentRuntimeError()
}
nextRuntimeError moves to the next error if available.
{
if errIdx < len(errList)-1 {
errIdx++
}
return currentRuntimeError()
}
resetRuntimeErrors clears all tracked errors.
{
errList = nil
errIdx = -1
}
runtimeErrorCount returns the number of stored errors.
{ return len(errList) }
runtimeErrorIndex returns the current error index.
{ return errIdx }
{
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()
}
}
{
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()
}
{
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)
}
{
lines := strings.Split(stack, "\n")
for _, l := range lines {
if strings.Contains(l, ".go") {
l = strings.TrimSpace(l)
return l
}
}
return ""
}
{
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{}{}
}
{
if componentID == "" {
return
}
storeUsageMu.Lock()
delete(storeUsage, componentID)
storeUsageMu.Unlock()
}
{
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
}
{
storeUsageMu.Lock()
storeUsage = map[string]map[string]map[string]map[string]struct{}{}
storeUsageMu.Unlock()
}
{
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
}
{
if id == "" {
return
}
lifecycleMu.Lock()
delete(lifecycleByComponent, id)
lifecycleMu.Unlock()
}
{
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
}
{
lifecycleMu.Lock()
lifecycleByComponent = map[string][]lifecycleEvent{}
lifecycleMu.Unlock()
}
{
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
}
{
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)
}
{
mu.Lock()
roots = nil
nodes = map[string]*node{}
nextID = 0
mu.Unlock()
}
{
mu.RLock()
defer mu.RUnlock()
b, _ := json.Marshal(roots)
return string(b)
}
{
resetTree()
walk(c, "")
}
{
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)
}
}
{
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()
}
{
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
}
{
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
}
{
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())
}
{
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
}
{
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
}
{
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
}
}
{
if n == nil || componentID == "" {
return
}
if bindings := snapshotStoreBindings(componentID); len(bindings) > 0 {
n.StoreBindings = bindings
}
}
{
if d <= 0 {
return 0
}
return float64(d) / float64(time.Millisecond)
}
{
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
}
{
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
}
}
}
{
if !v.IsValid() || n.Store != nil {
return
}
if snap := extractStore(v, "Store"); snap != nil {
n.Store = snap
}
}
{
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
}
{
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
}
{
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 ""
}
{
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
}
{
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)
}
{
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{}
}
{
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
}
{
if len(in) == 0 {
return nil
}
out := make([]any, len(in))
for i, v := range in {
out[i] = sanitizeValue(v)
}
return out
}
{
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)
}
{
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)
}
{
switch kind {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
return true
default:
return false
}
}
{ return "" }
{ return m.name }
{ return m.id }
{
return m.stats
}
{
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)
}
}
{
resetLifecycles()
root := &mockComponent{name: "A", id: "a"}
captureTree(root)
js := treeJSON()
if js == "" || js[0] != '[' {
t.Fatalf("unexpected json: %s", js)
}
}
{
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)
}
}
{
dup := make(map[string]any, len(s.state))
for k, v := range s.state {
dup[k] = v
}
return dup
}
{ return s.module }
{ return s.name }
{
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)
}
}
{
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)
}
}
{
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)
}
}
{ return nil }
{
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()
}
}
{
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)
}
import "fmt"
import "strings"
import "github.com/rfwlab/rfw/v1/composition"
import "github.com/rfwlab/rfw/v1/dom"
import "github.com/rfwlab/rfw/v1/js"
js
import "sort"
import "sync"
import "sort"
import "sync"
import "time"
import "encoding/json"
import "sync"
import "encoding/json"
import "fmt"
import "reflect"
import "sort"
import "time"
import "github.com/rfwlab/rfw/v1/core"
import "github.com/rfwlab/rfw/v1/dom"
import "math"
import "testing"
import "time"
import "github.com/rfwlab/rfw/v1/core"
import "encoding/json"
import "github.com/rfwlab/rfw/v1/core"
import "github.com/rfwlab/rfw/v1/dom"
import "github.com/rfwlab/rfw/v1/http"
import "github.com/rfwlab/rfw/v1/js"
import "github.com/rfwlab/rfw/v1/router"
import "github.com/rfwlab/rfw/v1/state"
import "time"