diff --git a/plan/storage/memory.go b/plan/storage/memory.go index 2100eb7..ce08fb7 100644 --- a/plan/storage/memory.go +++ b/plan/storage/memory.go @@ -9,13 +9,15 @@ import ( ) type Memory struct { - events map[string]item.Event - mutex sync.RWMutex + events map[string]item.Event + localIDs map[int]string + mutex sync.RWMutex } func NewMemory() *Memory { return &Memory{ - events: make(map[string]item.Event), + events: make(map[string]item.Event), + localIDs: make(map[int]string), } } @@ -30,7 +32,23 @@ func (r *Memory) Find(id string) (item.Event, error) { return event, nil } -func (r *Memory) FindAll() ([]item.Event, error) { +func (r *Memory) FindByLocal(localID int) (item.Event, error) { + r.mutex.RLock() + defer r.mutex.RUnlock() + + id, exists := r.localIDs[localID] + if !exists { + return item.Event{}, errors.New("event not found") + } + + event, exists := r.events[id] + if !exists { + return item.Event{}, errors.New("id an localid mismatch") + } + return event, nil +} + +func (r *Memory) FindAll() (map[int]string, []item.Event, error) { r.mutex.RLock() defer r.mutex.RUnlock() @@ -42,14 +60,24 @@ func (r *Memory) FindAll() ([]item.Event, error) { return events[i].ID < events[j].ID }) - return events, nil + return r.localIDs, events, nil } func (r *Memory) Store(e item.Event) error { r.mutex.Lock() defer r.mutex.Unlock() + if _, exists := r.events[e.ID]; !exists { + cur := make([]int, 0, len(r.localIDs)) + for i := range r.localIDs { + cur = append(cur, i) + } + localID := NextLocalID(cur) + r.localIDs[localID] = e.ID + } + r.events[e.ID] = e + return nil } @@ -61,5 +89,12 @@ func (r *Memory) Delete(id string) error { return errors.New("event not found") } delete(r.events, id) + + for localID, eventID := range r.localIDs { + if id == eventID { + delete(r.localIDs, localID) + } + } + return nil } diff --git a/plan/storage/storage.go b/plan/storage/storage.go index 3e8369d..335e220 100644 --- a/plan/storage/storage.go +++ b/plan/storage/storage.go @@ -1,6 +1,10 @@ package storage -import "go-mod.ewintr.nl/planner/item" +import ( + "sort" + + "go-mod.ewintr.nl/planner/item" +) type EventRepo interface { Store(event item.Event) error @@ -8,3 +12,39 @@ type EventRepo interface { FindAll() ([]item.Event, error) Delete(id string) error } + +func NextLocalID(used []int) int { + if len(used) == 0 { + return 1 + } + + sort.Ints(used) + usedMax := 1 + for _, u := range used { + if u > usedMax { + usedMax = u + } + } + + var limit int + for limit = 1; limit <= len(used) || limit < usedMax; limit *= 10 { + } + + newId := used[len(used)-1] + 1 + if newId < limit { + return newId + } + + usedMap := map[int]bool{} + for _, u := range used { + usedMap[u] = true + } + + for i := 1; i < limit; i++ { + if _, ok := usedMap[i]; !ok { + return i + } + } + + return limit +} diff --git a/plan/storage/storage_test.go b/plan/storage/storage_test.go new file mode 100644 index 0000000..528ec1a --- /dev/null +++ b/plan/storage/storage_test.go @@ -0,0 +1,76 @@ +package storage_test + +import ( + "testing" + + "go-mod.ewintr.nl/planner/plan/storage" +) + +func TestNextLocalId(t *testing.T) { + for _, tc := range []struct { + name string + used []int + exp int + }{ + { + name: "empty", + used: []int{}, + exp: 1, + }, + { + name: "not empty", + used: []int{5}, + exp: 6, + }, + { + name: "multiple", + used: []int{2, 3, 4}, + exp: 5, + }, + { + name: "holes", + used: []int{1, 5, 8}, + exp: 9, + }, + { + name: "expand limit", + used: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + exp: 11, + }, + { + name: "wrap if possible", + used: []int{8, 9}, + exp: 1, + }, + { + name: "find hole", + used: []int{1, 2, 3, 4, 5, 7, 8, 9}, + exp: 6, + }, + { + name: "dont wrap if expanded before", + used: []int{15, 16}, + exp: 17, + }, + { + name: "do wrap if expanded limit is reached", + used: []int{99}, + exp: 1, + }, + { + name: "sync bug", + used: []int{151, 956, 955, 150, 154, 155, 145, 144, + 136, 152, 148, 146, 934, 149, 937, 135, 140, 139, + 143, 137, 153, 939, 138, 953, 147, 141, 938, 142, + }, + exp: 957, + }, + } { + t.Run(tc.name, func(t *testing.T) { + act := storage.NextLocalID(tc.used) + if tc.exp != act { + t.Errorf("exp %v, got %v", tc.exp, act) + } + }) + } +}