App Cache

An app-level cache stores data for the entire duration of the application's execution, from launch to termination.

This cache is not provided by ToolGUI and needs to be implemented by the developer.

For example:

type App struct {
	data sync.Map
}

func (app *App) QueryData(key string) any {
	if v, ok := app.data.Load(key); ok {
		return v
	}

	// calculate the value, then keep the first one stored
	calVal := calculate(key)
	v, _ := app.data.LoadOrStore(key, calVal)
	return v
}

func (app *App) Page1(p *tgframe.Params) error {
	tgcomp.Text(p.Main, app.QueryData("key").(string))
	return nil
}

Loading in the background

A cache that takes long to fill should not hold up the service. Start the service right away, fill the cache in a goroutine, and let the page draw a loading screen until it is ready. When the load finishes, RerunAll reruns the pages already open, so they show the data without the user doing anything:

type App struct {
	tgApp *tgframe.App

	mu   sync.RWMutex
	data *Data // nil until loaded
	err  error
}

func (app *App) load() {
	data, err := loadData() // slow

	app.mu.Lock()
	app.data, app.err = data, err
	app.mu.Unlock()

	// Pages opened during the load still show the loading screen.
	app.tgApp.RerunAll()
}

// snapshot returns what is loaded so far; data is nil while loading.
func (app *App) snapshot() (*Data, error) {
	app.mu.RLock()
	defer app.mu.RUnlock()
	return app.data, app.err
}

func (app *App) Page1(p *tgframe.Params) error {
	data, err := app.snapshot()
	if err != nil {
		return err
	}

	if data == nil {
		tgcomp.Spinner(p.Main, "Loading data...")
		return nil
	}

	tgcomp.Text(p.Main, data.Summary())
	return nil
}

func main() {
	tgApp := tgframe.NewApp()
	app := &App{tgApp: tgApp}
	tgApp.AddPage("page1", "Page1", app.Page1)

	go app.load()

	// Serves at once; load() keeps running in the background.
	tgexec.NewWebExecutor(tgApp).StartService("127.0.0.1:3000")
}

The page func must not wait for the load: draw the loading screen and return. A page blocked on the load keeps its session busy, and the user gets no answer until it finishes. Store the loaded data with one assignment, as load() does, so a run sees either nothing or all of it.

RerunAll returns at once. It never cuts a run in flight, since that would drop the button click the run is handling: a page in the middle of a run is rerun once that run ends, and calls made meanwhile fold into that one rerun. RerunPage("page1") reruns only the pages named.

A reload button

The same call makes a "reload data" button that updates every open page, not only the one it was pressed on:

func (app *App) Page1(p *tgframe.Params) error {
	if tgcomp.Button(p.Main, "Reload data") {
		go app.load()
	}

	// ...draw from app.snapshot() as above
	return nil
}

Load in a goroutine rather than in the page func, so the run that handled the click ends at once. Pages, the one with the button included, rerun with the new data when load() calls RerunAll. To show the loading screen while it runs, clear app.data before starting the load.

In wasm

In a wasm build the process is one browser tab, so a Go global is the app state for that tab and for one user. That is a valid place for data the page needs across page switches, for example the submissions made so far. It lasts until the tab reloads or closes.

For data that has to survive a reload, such as drafts and records, use the persistent store.

Additional Considerations:

  • Cache Invalidation: As the application runs, the underlying data sources might change. It's crucial to have a strategy to invalidate cached data when necessary to ensure consistency. This could involve periodically refreshing the cache or implementing mechanisms to detect changes in the source data.

  • Memory Usage: App-level caches can consume memory. It's essential to choose appropriate data structures and cache eviction policies to balance performance gains with memory constraints.