Fixed concurrency issue; moved api instance into app

This commit is contained in:
tyler 2023-12-21 15:20:43 -05:00
parent 0e97fe4ea7
commit d329c36653
7 changed files with 88 additions and 68 deletions

58
app.go
View file

@ -7,8 +7,9 @@ import (
"log" "log"
"os" "os"
"sync" "sync"
"time"
"github.com/tylertravisty/go-utils/random" "github.com/tylertravisty/rum-goggles/internal/api"
"github.com/tylertravisty/rum-goggles/internal/config" "github.com/tylertravisty/rum-goggles/internal/config"
rumblelivestreamlib "github.com/tylertravisty/rumble-livestream-lib-go" rumblelivestreamlib "github.com/tylertravisty/rumble-livestream-lib-go"
) )
@ -22,17 +23,20 @@ type App struct {
ctx context.Context ctx context.Context
cfg *config.App cfg *config.App
cfgMu sync.Mutex cfgMu sync.Mutex
api *api.Api
apiMu sync.Mutex
} }
// NewApp creates a new App application struct // NewApp creates a new App application struct
func NewApp() *App { func NewApp() *App {
return &App{} return &App{api: api.NewApi()}
} }
// startup is called when the app starts. The context is saved // startup is called when the app starts. The context is saved
// so we can call the runtime methods // so we can call the runtime methods
func (a *App) startup(ctx context.Context) { func (a *App) startup(ctx context.Context) {
a.ctx = ctx a.ctx = ctx
a.api.Startup(ctx)
err := a.loadConfig() err := a.loadConfig()
if err != nil { if err != nil {
// TODO: handle error better on startup // TODO: handle error better on startup
@ -55,7 +59,7 @@ func (a *App) loadConfig() error {
} }
func (a *App) newConfig() error { func (a *App) newConfig() error {
cfg := &config.App{Channels: []config.Channel{}} cfg := &config.App{Channels: map[string]config.Channel{}}
err := cfg.Save(configFilepath) err := cfg.Save(configFilepath)
if err != nil { if err != nil {
return fmt.Errorf("error saving new config: %v", err) return fmt.Errorf("error saving new config: %v", err)
@ -93,11 +97,15 @@ func (a *App) AddChannel(url string) (*config.App, error) {
name = resp.ChannelName name = resp.ChannelName
} }
channel := config.Channel{ApiUrl: url, Name: name}
a.cfgMu.Lock() a.cfgMu.Lock()
defer a.cfgMu.Unlock() defer a.cfgMu.Unlock()
a.cfg.Channels = append(a.cfg.Channels, channel) _, err = a.cfg.NewChannel(url, name)
if err != nil {
// TODO: log error
fmt.Println("error creating new channel:", err)
return nil, fmt.Errorf("error creating new channel")
}
err = a.cfg.Save(configFilepath) err = a.cfg.Save(configFilepath)
if err != nil { if err != nil {
// TODO: log error // TODO: log error
@ -108,26 +116,24 @@ func (a *App) AddChannel(url string) (*config.App, error) {
return a.cfg, nil return a.cfg, nil
} }
// Greet returns a greeting for the given name func (a *App) StartApi(cid string) error {
func (a *App) Greet(name string) string { channel, found := a.cfg.Channels[cid]
random, err := random.String(10) if !found {
if err != nil { // TODO: log error
fmt.Println("random.Alphabetic err:", err) fmt.Println("could not find channel CID:", cid)
return name return fmt.Errorf("channel CID not found")
} }
//return fmt.Sprintf("Hello %s, It's show time!", name)
return fmt.Sprintf("Hello %s, It's show time!", random) err := a.api.Start(channel.ApiUrl, channel.Interval*time.Second)
if err != nil {
// TODO: log error
fmt.Println("error starting api:", err)
return fmt.Errorf("error starting API")
}
return nil
} }
// func (a *App) QueryAPI(url string) (*rumblelivestreamlib.Followers, error) { func (a *App) StopApi() {
// fmt.Println("QueryAPI") a.api.Stop()
// client := rumblelivestreamlib.Client{StreamKey: url} }
// resp, err := client.Request()
// if err != nil {
// // TODO: log error
// fmt.Println("client.Request err:", err)
// return nil, fmt.Errorf("API request failed")
// }
// return &resp.Followers, nil
// }

View file

@ -3,8 +3,13 @@ import './ChannelList.css';
function ChannelList(props) { function ChannelList(props) {
const sortChannelsAlpha = () => { const sortChannelsAlpha = () => {
let sorted = [...props.channels].sort((a, b) => let keys = Object.keys(props.channels);
a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1 // let sorted = [...props.channels].sort((a, b) =>
// a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1
// );
let sorted = [...keys].sort((a, b) =>
props.channels[a].name.toLowerCase() > props.channels[b].name.toLowerCase() ? 1 : -1
); );
return sorted; return sorted;
}; };
@ -17,9 +22,9 @@ function ChannelList(props) {
<div className='channel' style={index === 0 ? { borderTop: 'none' } : {}}> <div className='channel' style={index === 0 ? { borderTop: 'none' } : {}}>
<button <button
className='channel-button' className='channel-button'
onClick={() => props.openStreamDashboard(channel.api_url)} onClick={() => props.openStreamDashboard(props.channels[channel].id)}
> >
{channel.name} {props.channels[channel].name}
</button> </button>
</div> </div>
))} ))}

View file

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Navigate, useLocation, useNavigate } from 'react-router-dom'; import { Navigate, useLocation, useNavigate } from 'react-router-dom';
import { Start, Stop } from '../../wailsjs/go/api/Api'; import { StartApi, StopApi } from '../../wailsjs/go/main/App';
import './Dashboard.css'; import './Dashboard.css';
import { EventsEmit, EventsOn } from '../../wailsjs/runtime/runtime'; import { EventsEmit, EventsOn } from '../../wailsjs/runtime/runtime';
@ -17,7 +17,7 @@ function Dashboard() {
const navigate = useNavigate(); const navigate = useNavigate();
const [refresh, setRefresh] = useState(false); const [refresh, setRefresh] = useState(false);
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
const [streamKey, setStreamKey] = useState(location.state.streamKey); const [cid, setCID] = useState(location.state.cid);
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [channelName, setChannelName] = useState(''); const [channelName, setChannelName] = useState('');
const [followers, setFollowers] = useState({}); const [followers, setFollowers] = useState({});
@ -41,7 +41,7 @@ function Dashboard() {
useEffect(() => { useEffect(() => {
console.log('use effect start'); console.log('use effect start');
Start(streamKey); StartApi(cid);
setActive(true); setActive(true);
EventsOn('QueryResponse', (response) => { EventsOn('QueryResponse', (response) => {
@ -77,7 +77,7 @@ function Dashboard() {
}, []); }, []);
const home = () => { const home = () => {
Stop() StopApi()
.then(() => setActive(false)) .then(() => setActive(false))
.then(() => { .then(() => {
navigate(NavSignIn); navigate(NavSignIn);
@ -89,7 +89,7 @@ function Dashboard() {
const startQuery = () => { const startQuery = () => {
console.log('start'); console.log('start');
Start(streamKey) StartApi(cid)
.then(() => { .then(() => {
setActive(true); setActive(true);
}) })
@ -100,7 +100,7 @@ function Dashboard() {
const stopQuery = () => { const stopQuery = () => {
console.log('stop'); console.log('stop');
Stop().then(() => { StopApi().then(() => {
setActive(false); setActive(false);
}); });
}; };

View file

@ -8,7 +8,7 @@ import ChannelList from '../components/ChannelList';
function SignIn() { function SignIn() {
const navigate = useNavigate(); const navigate = useNavigate();
const [config, setConfig] = useState({ channels: [] }); const [config, setConfig] = useState({ channels: {} });
const [streamKey, setStreamKey] = useState(''); const [streamKey, setStreamKey] = useState('');
const updateStreamKey = (event) => setStreamKey(event.target.value); const updateStreamKey = (event) => setStreamKey(event.target.value);
const [showStreamKey, setShowStreamKey] = useState(false); const [showStreamKey, setShowStreamKey] = useState(false);
@ -17,10 +17,10 @@ function SignIn() {
useEffect(() => { useEffect(() => {
Config() Config()
.then((response) => { .then((response) => {
console.log(response);
setConfig(response); setConfig(response);
}) })
.catch((err) => { .catch((err) => {
// TODO: display error to user
console.log('error getting config', err); console.log('error getting config', err);
}); });
}, []); }, []);
@ -37,8 +37,8 @@ function SignIn() {
}); });
}; };
const openStreamDashboard = (key) => { const openStreamDashboard = (cid) => {
navigate(NavDashboard, { state: { streamKey: key } }); navigate(NavDashboard, { state: { cid: cid } });
}; };
return ( return (

View file

@ -16,22 +16,17 @@ type Api struct {
cancelMu sync.Mutex cancelMu sync.Mutex
querying bool querying bool
queryingMu sync.Mutex queryingMu sync.Mutex
queryInterval time.Duration
queryIntervalMu sync.Mutex
} }
func NewApi() *Api { func NewApi() *Api {
return &Api{queryInterval: 10 * time.Second} return &Api{}
} }
func (a *Api) Startup(ctx context.Context) { func (a *Api) Startup(ctx context.Context) {
a.ctx = ctx a.ctx = ctx
runtime.EventsOn(ctx, "StopQuery", func(optionalData ...interface{}) {
a.Stop()
})
} }
func (a *Api) Start(url string) error { func (a *Api) Start(url string, interval time.Duration) error {
fmt.Println("Api.Start") fmt.Println("Api.Start")
if url == "" { if url == "" {
return fmt.Errorf("empty stream key") return fmt.Errorf("empty stream key")
@ -48,7 +43,7 @@ func (a *Api) Start(url string) error {
a.cancelMu.Lock() a.cancelMu.Lock()
a.cancel = cancel a.cancel = cancel
a.cancelMu.Unlock() a.cancelMu.Unlock()
a.start(ctx, url) go a.start(ctx, url, interval)
} else { } else {
fmt.Println("Querying already started") fmt.Println("Querying already started")
} }
@ -65,12 +60,9 @@ func (a *Api) Stop() {
a.cancelMu.Unlock() a.cancelMu.Unlock()
} }
func (a *Api) start(ctx context.Context, url string) { func (a *Api) start(ctx context.Context, url string, interval time.Duration) {
for { for {
a.query(url) a.query(url)
a.queryIntervalMu.Lock()
interval := a.queryInterval
a.queryIntervalMu.Unlock()
timer := time.NewTimer(interval) timer := time.NewTimer(interval)
select { select {
case <-ctx.Done(): case <-ctx.Done():

View file

@ -4,15 +4,39 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"time"
"github.com/tylertravisty/go-utils/random"
)
const (
CIDLen = 8
DefaultInterval = 10
) )
type Channel struct { type Channel struct {
ID string `json:"id"`
ApiUrl string `json:"api_url"` ApiUrl string `json:"api_url"`
Name string `json:"name"` Name string `json:"name"`
Interval time.Duration `json:"interval"`
}
func (a *App) NewChannel(url string, name string) (string, error) {
for {
id, err := random.String(CIDLen)
if err != nil {
return "", fmt.Errorf("config: error generating ID: %v", err)
}
if _, exists := a.Channels[id]; !exists {
a.Channels[id] = Channel{id, url, name, DefaultInterval}
return id, nil
}
}
} }
type App struct { type App struct {
Channels []Channel `json:"channels"` Channels map[string]Channel `json:"channels"`
} }
func Load(filepath string) (*App, error) { func Load(filepath string) (*App, error) {

View file

@ -1,10 +1,8 @@
package main package main
import ( import (
"context"
"embed" "embed"
"github.com/tylertravisty/rum-goggles/internal/api"
"github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver" "github.com/wailsapp/wails/v2/pkg/options/assetserver"
@ -15,7 +13,6 @@ var assets embed.FS
func main() { func main() {
// Create an instance of the app structure // Create an instance of the app structure
api := api.NewApi()
app := NewApp() app := NewApp()
// Create application with options // Create application with options
@ -27,13 +24,9 @@ func main() {
Assets: assets, Assets: assets,
}, },
BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255}, BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255},
OnStartup: func(ctx context.Context) { OnStartup: app.startup,
app.startup(ctx)
api.Startup(ctx)
},
Bind: []interface{}{ Bind: []interface{}{
app, app,
api,
}, },
}) })