Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 61 additions & 24 deletions august.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type August struct {
mu sync.RWMutex
storeRegistry map[string]reflect.Type // A map registrying the store types
config AugustConfig // August configuration
storage map[string]AugustStore // A map of all the stores
storage map[string]*AugustStore // A map of all the stores
eventFunc AugustEventFunc // A function to call when an event happens
systemModCache []string // Every time we modify a file, we add info about it so that FSNotify doesn't trigger on it
}
Expand Down Expand Up @@ -62,7 +62,7 @@ func Init() *August {
Format: "json",
FSNotify: true,
}
storage := make(map[string]AugustStore)
storage := make(map[string]*AugustStore)

a := &August{
storeRegistry: stores,
Expand All @@ -81,18 +81,45 @@ func (a *August) Verbose() {
}

// Set a config option.
func (a *August) Config(k AugustConfigOption, v interface{}) {
func (a *August) Config(k AugustConfigOption, v interface{}) error {
a.mu.Lock()
defer a.mu.Unlock()
Comment on lines 83 to 86
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing Config to return an error is a breaking API change for downstream users (previously it was void). If you need to keep compatibility, consider adding a new method (e.g., ConfigE/SetConfig) that returns an error while keeping the old signature as a wrapper (possibly logging or panicking on invalid types), or bumping the module major version if breaking changes are intended.

Copilot uses AI. Check for mistakes.
log.Printf("Setting config: %s to %v", k, v)

if k == Config_Verbose && v.(bool) {
// set verbose mode if we configure that
a.Verbose()
switch k {
case Config_StorageDir:
val, ok := v.(string)
if !ok {
return fmt.Errorf("config option %s requires a string value", k)
}
a.config.StorageDir = val
case Config_Verbose:
val, ok := v.(bool)
if !ok {
return fmt.Errorf("config option %s requires a bool value", k)
}
a.config.Verbose = val
if val {
a.Verbose()
}
case Config_Format:
val, ok := v.(string)
if !ok {
return fmt.Errorf("config option %s requires a string value", k)
}
a.config.Format = val
case Config_FSNotify:
val, ok := v.(bool)
if !ok {
return fmt.Errorf("config option %s requires a bool value", k)
}
a.config.FSNotify = val
default:
return fmt.Errorf("unknown config option: %s", k)
}

reflect.ValueOf(&a.config).Elem().FieldByName(k.String()).Set(reflect.ValueOf(v))
log.Printf("Config: %+v", a.config)
return nil
}

func (a *August) SetEventFunc(f AugustEventFunc) {
Expand Down Expand Up @@ -129,13 +156,12 @@ func (a *August) Unmarshal(input []byte, output interface{}) error {

// Get a store by name.
func (a *August) GetStore(name string) (*AugustStore, error) {
a.mu.Lock()
defer a.mu.Unlock()
a.mu.RLock()
defer a.mu.RUnlock()
if store, ok := a.storage[name]; ok {
return &store, nil
} else {
return &AugustStore{}, fmt.Errorf("data store %s not found", name)
return store, nil
}
return nil, fmt.Errorf("data store %s not found", name)
}

// Register a store.
Expand All @@ -145,7 +171,7 @@ func (a *August) Register(name string, store interface{}) {
log.Printf("Registering store: %s of type %T", name, store)

a.storeRegistry[name] = reflect.TypeOf(store)
a.storage[name] = AugustStore{
a.storage[name] = &AugustStore{
name: name,
parent: a,
data: make(map[string]AugustStoreDataset),
Expand All @@ -154,29 +180,38 @@ func (a *August) Register(name string, store interface{}) {

// Populate registry is used during initial startup to load any existing data.
func (a *August) populateRegistry(name string) error {
a.mu.Lock()
defer a.mu.Unlock()
if _, ok := a.storeRegistry[name]; !ok {
a.mu.RLock()
_, ok := a.storeRegistry[name]
store := a.storage[name]
ext := "." + a.config.Format
storageDir := a.config.StorageDir
a.mu.RUnlock()

if !ok {
return fmt.Errorf("store %s does not exists", name)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error message grammar: "does not exists" should be "does not exist".

Suggested change
return fmt.Errorf("store %s does not exists", name)
return fmt.Errorf("store %s does not exist", name)

Copilot uses AI. Check for mistakes.
}

// check the directory for files and load them
dir, err := os.ReadDir(a.config.StorageDir + "/" + name)
dir, err := os.ReadDir(storageDir + "/" + name)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}

store := a.storage[name]

for _, file := range dir {
// skip invalid files
if file.IsDir() || file.Type().IsRegular() && file.Name()[len(file.Name())-len(a.config.Format):] != a.config.Format {
// skip directories and files that don't have the expected extension
if file.IsDir() {
continue
}
fname := file.Name()
if !strings.HasSuffix(fname, ext) {
continue
}

id := file.Name()[:len(file.Name())-len(a.config.Format)-1]
log.Printf("Loading file: %s for registry %s as ID %s", file.Name(), name, id)
// read the file
id := fname[:len(fname)-len(ext)]
log.Printf("Loading file: %s for registry %s as ID %s", fname, name, id)
store.loadFromFile(id)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

populateRegistry calls store.loadFromFile(id) but ignores the returned error, which can silently skip corrupted/unreadable entries and leave the in-memory store partially populated. Please handle/propagate the error (or at least log it) so startup failures are visible to callers.

Suggested change
store.loadFromFile(id)
if err := store.loadFromFile(id); err != nil {
log.Printf("Error loading file: %s for registry %s as ID %s: %v", fname, name, id, err)
return err
}

Copilot uses AI. Check for mistakes.
}
return nil
Expand Down Expand Up @@ -311,6 +346,8 @@ func (a *August) Run() error {
// detected, and returns true + deletes the entry if it does.
func (a *August) handleModCacheSkip(method, name, id string) bool {
cacheName := fmt.Sprintf("%s::%s::%s", method, name, id)
a.mu.Lock()
defer a.mu.Unlock()
for i, v := range a.systemModCache {
if v == cacheName {
log.Printf("[FS Notify] Found %s, skipping FS modify actions", cacheName)
Expand Down
8 changes: 6 additions & 2 deletions augustStore.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ func (as *AugustStore) GetIds() []string {

// GetAll returns all values in the store.
func (as *AugustStore) GetAll() (map[string]interface{}, error) {
as.mu.RLock()
defer as.mu.RUnlock()

if len((*as).data) == 0 {
return nil, fmt.Errorf("no data found for store: %s", (*as).name)
Expand All @@ -144,8 +146,6 @@ func (as *AugustStore) GetAll() (map[string]interface{}, error) {
newSet := make(map[string]interface{})

for id, val := range (*as).data {
as.mu.RLock()
defer as.mu.RUnlock()
newSet[id] = val.data
}

Expand Down Expand Up @@ -248,7 +248,11 @@ func (as *AugustStore) ValidateId(id string) error {
func (as *AugustStore) event(name string, id string) {
cacheName := fmt.Sprintf("%s::%s::%s", name, (*as).name, id)
log.Printf("[EVENT FIRED] %s", cacheName)
as.parent.mu.Lock()
as.parent.systemModCache = append(as.parent.systemModCache, cacheName)
as.parent.mu.Unlock()
// eventFunc is invoked outside the lock to prevent deadlocks if the
// callback re-enters August methods that acquire the same mutex.
(*as).parent.eventFunc(name, (*as).name, id)
}

Expand Down
6 changes: 2 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ module github.com/solafide-dev/august
go 1.20

require (
github.com/fsnotify/fsnotify v1.6.0
github.com/google/uuid v1.3.1
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/fsnotify/fsnotify v1.6.0 // indirect
golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect
)
require golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect
Loading