Options struct

Fields:

  • Normalize (func(string) string)
  • PrefetchLimit (int)
  • PrefetchThreshold (float64)
  • Channel (string)

TransitionProbability struct

Fields:

  • From (string)
  • To (string)
  • Count (int)
  • Probability (float64)

Plugin struct

Fields:

  • opts (Options)
  • tracker (*transitionTracker)
  • prefetch (prefetcher)

Methods:

Name


Returns:
  • string

Show/Hide Method Body
{
	return "routeranalytics"
}

Build


Parameters:
  • json.RawMessage

Returns:
  • error

Show/Hide Method Body
{ return nil }

Install


Parameters:
  • a *core.App

Show/Hide Method Body
{
	if a == nil {
		return
	}
	setDefaultInstance(p)
	a.RegisterRouter(func(path string) {
		p.handleNavigation(path)
	})
}

TransitionProbabilities


Parameters:
  • from string

Returns:
  • []TransitionProbability

Show/Hide Method Body
{
	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
}

MostLikelyNext


Parameters:
  • from string
  • limit int

Returns:
  • []TransitionProbability

Show/Hide Method Body
{
	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
}

Reset


Show/Hide Method Body
{
	if p == nil {
		return
	}
	p.tracker.reset()
	if p.prefetch != nil {
		p.prefetch.reset()
	}
}

handleNavigation


Parameters:
  • path string

Show/Hide Method Body
{
	normalized := strings.TrimSpace(path)
	if p.opts.Normalize != nil {
		normalized = p.opts.Normalize(path)
	}
	if normalized == "" {
		return
	}
	p.tracker.visit(normalized)
	p.enqueuePrefetch(normalized)
}

enqueuePrefetch


Parameters:
  • current string

Show/Hide Method Body
{
	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)
}

New function

Parameters:

  • opts Options

Returns:

  • *Plugin

References:

  • Options (v1/plugins/routeranalytics)
Show/Hide Function Body
{
	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
}

TransitionProbabilities function

Parameters:

  • from string

Returns:

  • []TransitionProbability
Show/Hide Function Body
{
	if inst := getDefaultInstance(); inst != nil {
		return inst.TransitionProbabilities(from)
	}
	return nil
}

MostLikelyNext function

Parameters:

  • from string
  • limit int

Returns:

  • []TransitionProbability
Show/Hide Function Body
{
	if inst := getDefaultInstance(); inst != nil {
		return inst.MostLikelyNext(from, limit)
	}
	return nil
}

Reset function

Show/Hide Function Body
{
	if inst := getDefaultInstance(); inst != nil {
		inst.Reset()
	}
}

transitionTracker struct

Fields:

  • mu (sync.RWMutex)
  • transitions (map[string]map[string]int)
  • totals (map[string]int)
  • last (string)

Methods:

visit


Parameters:
  • routeID string

Show/Hide Method Body
{
	if routeID == "" {
		return
	}
	t.mu.Lock()
	if t.last != "" {
		t.recordLocked(t.last, routeID)
	}
	t.last = routeID
	t.mu.Unlock()
}

recordLocked


Parameters:
  • from string
  • to string

Show/Hide Method Body
{
	if from == "" || to == "" {
		return
	}
	if t.transitions[from] == nil {
		t.transitions[from] = make(map[string]int)
	}
	t.transitions[from][to]++
	t.totals[from]++
}

reset


Show/Hide Method Body
{
	t.mu.Lock()
	t.transitions = make(map[string]map[string]int)
	t.totals = make(map[string]int)
	t.last = ""
	t.mu.Unlock()
}

probabilities


Parameters:
  • from string

Returns:
  • []TransitionProbability

Show/Hide Method Body
{
	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
}

mostLikelyNext


Parameters:
  • from string
  • limit int

Returns:
  • []TransitionProbability

Show/Hide Method Body
{
	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
}

newTransitionTracker function

Returns:

  • *transitionTracker
Show/Hide Function Body
{
	return &transitionTracker{
		transitions: make(map[string]map[string]int),
		totals:      make(map[string]int),
	}
}

defaultNormalize function

Parameters:

  • path string

Returns:

  • string
Show/Hide Function Body
{
	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 function

NormalizePath applica la normalizzazione predefinita (trim, rimozione di query/hash e slash iniziale).

Parameters:

  • path string

Returns:

  • string
Show/Hide Function Body
{
	return defaultNormalize(path)
}

prefetcher interface

Methods:

request


Parameters:
  • resources []string

reset

noopPrefetcher struct

Methods:

request


Parameters:
  • []string

Show/Hide Method Body
{}

reset


Show/Hide Method Body
{}

setDefaultInstance function

Parameters:

  • p *Plugin
Show/Hide Function Body
{
	defaultMu.Lock()
	defaultInstance = p
	defaultMu.Unlock()
}

getDefaultInstance function

Returns:

  • *Plugin
Show/Hide Function Body
{
	defaultMu.RLock()
	defer defaultMu.RUnlock()
	return defaultInstance
}

TestTransitionProbabilities function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	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)
	}
}

TestMostLikelyNextOrdering function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	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)
	}
}

TestDefaultNormalize function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	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)
	}
}

prefetchSnapshot struct

Fields:

  • Resources ([]string)

wasmPrefetcher struct

Fields:

  • mu (sync.Mutex)
  • client (*netcode.Client[prefetchSnapshot])
  • tick (int64)
  • requested (map[string]struct{})
  • channel (string)

Implements:

  • prefetcher from routeranalytics

Methods:

request


Parameters:
  • resources []string

Show/Hide Method Body
{
	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)
}

reset


Show/Hide Method Body
{
	p.mu.Lock()
	p.requested = make(map[string]struct{})
	p.tick = 0
	p.client = nil
	p.mu.Unlock()
}

newPrefetcher function

Parameters:

  • channel string

Returns:

  • prefetcher

References:

Show/Hide Function Body
{
	return &wasmPrefetcher{
		channel:   channel,
		requested: make(map[string]struct{}),
	}
}

decodePrefetchState function

Parameters:

  • m map[string]any

Returns:

  • prefetchSnapshot

References:

Show/Hide Function Body
{
	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
}

interpPrefetchState function

Parameters:

  • _ prefetchSnapshot
  • next prefetchSnapshot
  • _ float64

Returns:

  • prefetchSnapshot

References:

Show/Hide Function Body
{
	return next
}

newPrefetcher function

Parameters:

  • string

Returns:

  • prefetcher

References:

Show/Hide Function Body
{ return noopPrefetcher{} }

encoding/json import

Import example:

import "encoding/json"

sort import

Import example:

import "sort"

strings import

Import example:

import "strings"

sync import

Import example:

import "sync"

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

Import example:

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

testing import

Import example:

import "testing"

sync import

Import example:

import "sync"

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

Import example:

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