{
return "routeranalytics"
}
{ return nil }
{
if a == nil {
return
}
setDefaultInstance(p)
a.RegisterRouter(func(path string) {
p.handleNavigation(path)
})
}
{
if p == nil {
return nil
}
probs := p.tracker.probabilities(from)
if len(probs) == 0 {
return nil
}
out := make([]TransitionProbability, len(probs))
copy(out, probs)
return out
}
{
if p == nil || limit <= 0 {
return nil
}
probs := p.tracker.mostLikelyNext(from, limit)
if len(probs) == 0 {
return nil
}
out := make([]TransitionProbability, len(probs))
copy(out, probs)
return out
}
{
if p == nil {
return
}
p.tracker.reset()
if p.prefetch != nil {
p.prefetch.reset()
}
}
{
normalized := strings.TrimSpace(path)
if p.opts.Normalize != nil {
normalized = p.opts.Normalize(path)
}
if normalized == "" {
return
}
p.tracker.visit(normalized)
p.enqueuePrefetch(normalized)
}
{
if p.opts.PrefetchLimit <= 0 || p.prefetch == nil {
return
}
predictions := p.tracker.mostLikelyNext(current, p.opts.PrefetchLimit)
if len(predictions) == 0 {
return
}
threshold := p.opts.PrefetchThreshold
if threshold < 0 {
threshold = 0
}
resources := make([]string, 0, len(predictions))
for _, prob := range predictions {
if prob.Probability < threshold {
continue
}
resources = append(resources, prob.To)
}
if len(resources) == 0 {
return
}
p.prefetch.request(resources)
}
{
if opts.Normalize == nil {
opts.Normalize = defaultNormalize
}
if opts.PrefetchLimit < 0 {
opts.PrefetchLimit = 0
} else if opts.PrefetchLimit == 0 {
opts.PrefetchLimit = 3
}
if opts.PrefetchThreshold <= 0 {
opts.PrefetchThreshold = 0.2
}
if opts.Channel == "" {
opts.Channel = "RouterPrefetch"
}
plugin := &Plugin{
opts: opts,
tracker: newTransitionTracker(),
}
if opts.PrefetchLimit > 0 {
plugin.prefetch = newPrefetcher(opts.Channel)
} else {
plugin.prefetch = noopPrefetcher{}
}
return plugin
}
{
if inst := getDefaultInstance(); inst != nil {
return inst.TransitionProbabilities(from)
}
return nil
}
{
if inst := getDefaultInstance(); inst != nil {
return inst.MostLikelyNext(from, limit)
}
return nil
}
{
if inst := getDefaultInstance(); inst != nil {
inst.Reset()
}
}
{
if routeID == "" {
return
}
t.mu.Lock()
if t.last != "" {
t.recordLocked(t.last, routeID)
}
t.last = routeID
t.mu.Unlock()
}
{
if from == "" || to == "" {
return
}
if t.transitions[from] == nil {
t.transitions[from] = make(map[string]int)
}
t.transitions[from][to]++
t.totals[from]++
}
{
t.mu.Lock()
t.transitions = make(map[string]map[string]int)
t.totals = make(map[string]int)
t.last = ""
t.mu.Unlock()
}
{
t.mu.RLock()
total := t.totals[from]
if total == 0 {
t.mu.RUnlock()
return nil
}
counts := t.transitions[from]
result := make([]TransitionProbability, 0, len(counts))
for to, count := range counts {
result = append(result, TransitionProbability{
From: from,
To: to,
Count: count,
Probability: float64(count) / float64(total),
})
}
t.mu.RUnlock()
sort.Slice(result, func(i, j int) bool {
if result[i].Probability == result[j].Probability {
return result[i].To < result[j].To
}
return result[i].Probability > result[j].Probability
})
return result
}
{
if limit <= 0 {
return nil
}
probs := t.probabilities(from)
if len(probs) == 0 {
return nil
}
if limit >= len(probs) {
out := make([]TransitionProbability, len(probs))
copy(out, probs)
return out
}
out := make([]TransitionProbability, limit)
copy(out, probs[:limit])
return out
}
{
return &transitionTracker{
transitions: make(map[string]map[string]int),
totals: make(map[string]int),
}
}
{
trimmed := strings.TrimSpace(path)
if trimmed == "" {
return ""
}
if idx := strings.IndexAny(trimmed, "?#"); idx >= 0 {
trimmed = trimmed[:idx]
}
if trimmed == "" {
return ""
}
if !strings.HasPrefix(trimmed, "/") {
trimmed = "/" + trimmed
}
return trimmed
}
NormalizePath applica la normalizzazione predefinita (trim, rimozione di query/hash e slash iniziale).
{
return defaultNormalize(path)
}
{}
{}
{
defaultMu.Lock()
defaultInstance = p
defaultMu.Unlock()
}
{
defaultMu.RLock()
defer defaultMu.RUnlock()
return defaultInstance
}
{
p := New(Options{})
p.Reset()
p.handleNavigation("/home")
p.handleNavigation("/posts")
p.handleNavigation("/home")
p.handleNavigation("/posts")
p.handleNavigation("/settings")
p.handleNavigation("/posts")
p.handleNavigation("/home")
probs := p.TransitionProbabilities("/posts")
if len(probs) != 2 {
t.Fatalf("expected two transitions, got %d", len(probs))
}
if probs[0].To != "/home" {
t.Fatalf("expected /home to be most probable, got %s", probs[0].To)
}
if probs[0].Count != 2 || probs[1].Count != 1 {
t.Fatalf("expected counts to reflect visits, got %v", probs)
}
if got := p.TransitionProbabilities("/missing"); got != nil {
t.Fatalf("expected nil for unknown route, got %v", got)
}
}
{
p := New(Options{})
p.Reset()
p.handleNavigation("/a")
p.handleNavigation("/b")
p.handleNavigation("/c")
p.handleNavigation("/b")
p.handleNavigation("/c")
p.handleNavigation("/d")
p.handleNavigation("/b")
p.handleNavigation("/d")
probs := p.MostLikelyNext("/b", 2)
if len(probs) != 2 {
t.Fatalf("expected two results, got %d", len(probs))
}
if probs[0].To != "/c" {
t.Fatalf("expected /c to be first, got %s", probs[0].To)
}
if probs[0].Probability <= probs[1].Probability {
t.Fatalf("expected probability ordering")
}
if limited := p.MostLikelyNext("/b", 1); len(limited) != 1 || limited[0].To != "/c" {
t.Fatalf("expected limit to return most probable route")
}
if none := p.MostLikelyNext("/z", 3); none != nil {
t.Fatalf("expected nil for unknown transitions, got %v", none)
}
}
{
p := New(Options{})
normalized := p.opts.Normalize(" docs/view?id=7#section ")
if normalized != "/docs/view" {
t.Fatalf("expected normalized path, got %q", normalized)
}
if global := NormalizePath(" docs/view?id=7#section "); global != normalized {
t.Fatalf("expected NormalizePath to match default normalize, got %q", global)
}
}
{
if len(resources) == 0 {
return
}
p.mu.Lock()
defer p.mu.Unlock()
if p.requested == nil {
p.requested = make(map[string]struct{})
}
filtered := make([]string, 0, len(resources))
for _, r := range resources {
if r == "" {
continue
}
if _, ok := p.requested[r]; ok {
continue
}
p.requested[r] = struct{}{}
filtered = append(filtered, r)
}
if len(filtered) == 0 {
return
}
if p.client == nil {
p.client = netcode.NewClient[prefetchSnapshot](p.channel, decodePrefetchState, interpPrefetchState)
}
p.tick++
p.client.Enqueue(map[string]any{"resources": filtered})
p.client.Flush(p.tick)
}
{
p.mu.Lock()
p.requested = make(map[string]struct{})
p.tick = 0
p.client = nil
p.mu.Unlock()
}
{
return &wasmPrefetcher{
channel: channel,
requested: make(map[string]struct{}),
}
}
{
raw, ok := m["resources"].([]any)
if !ok {
return prefetchSnapshot{}
}
out := prefetchSnapshot{Resources: make([]string, 0, len(raw))}
for _, item := range raw {
if s, ok := item.(string); ok {
out.Resources = append(out.Resources, s)
}
}
return out
}
{
return next
}
{ return noopPrefetcher{} }
import "encoding/json"
import "sort"
import "strings"
import "sync"
import "github.com/rfwlab/rfw/v1/core"
import "testing"
import "sync"
import "github.com/rfwlab/rfw/v1/netcode"