@@ -22,12 +22,15 @@ type CodeEntry struct {
2222}
2323
2424type Store struct {
25- mu sync.RWMutex
26- codes map [string ]* CodeEntry // code -> entry
27- appToCode map [string ]string // appID -> code
28- pregen chan string // optional pregen pool
29- ttl time.Duration
30- words []string
25+ mu sync.RWMutex
26+ codes map [string ]* CodeEntry // code -> entry
27+ appToCode map [string ]string // appID -> code
28+ pregen chan string // optional pregen pool
29+ ttl time.Duration
30+ words []string
31+ perm []int // permutation of [0..len(words)-1], set once
32+ singleIdx int // rotating cursor for single-word picks
33+ pairA , pairB int // rotating cursors for two-word pairs
3134}
3235
3336func NewStore (words []string , ttl time.Duration , pregenCap int ) * Store {
@@ -40,6 +43,19 @@ func NewStore(words []string, ttl time.Duration, pregenCap int) *Store {
4043 ttl : ttl ,
4144 words : words ,
4245 }
46+ if n := len (words ); n > 0 {
47+ s .perm = make ([]int , n )
48+ for i := 0 ; i < n ; i ++ {
49+ s .perm [i ] = i
50+ }
51+ // Fisher–Yates with crypto/rand
52+ for i := n - 1 ; i > 0 ; i -- {
53+ j , err := randInt (i + 1 )
54+ if err == nil && j != i {
55+ s .perm [i ], s .perm [j ] = s .perm [j ], s .perm [i ]
56+ }
57+ }
58+ }
4359 if pregenCap > 0 {
4460 s .pregen = make (chan string , pregenCap )
4561 go s .pregenerator ()
@@ -177,6 +193,84 @@ func (s *Store) newUniqueCode() (string, error) {
177193}
178194
179195func (s * Store ) generateCode () (string , error ) {
196+ // We need to see what's currently allocated, and advance our fair cursors.
197+ s .mu .Lock ()
198+ defer s .mu .Unlock ()
199+
200+ // 1) Try single-word pool.
201+ if code , ok := s .pickSingleLocked (); ok {
202+ return code , nil
203+ }
204+ // 2) All single words are currently allocated -> try two-word pool.
205+ if code , ok := s .pickPairLocked (); ok {
206+ return code , nil
207+ }
208+ // 3) Extremely unlikely to get here. Fall back to legacy generator (4 words + 4 digits).
209+ return s .generateLegacyUnlocked ()
210+ }
211+
212+ // pickSingleLocked returns an unused single-word code if available.
213+ // Requires s.mu to be held (write lock) because it advances s.singleIdx.
214+ func (s * Store ) pickSingleLocked () (string , bool ) {
215+ n := len (s .words )
216+ if n == 0 {
217+ return "" , false
218+ }
219+ if len (s .perm ) != n {
220+ // Safety: build a basic permutation if not set (shouldn't happen).
221+ s .perm = make ([]int , n )
222+ for i := range s .perm {
223+ s .perm [i ] = i
224+ }
225+ }
226+ for tried := 0 ; tried < n ; tried ++ {
227+ idx := s .perm [(s .singleIdx + tried )% n ]
228+ w := s .words [idx ]
229+ if _ , inUse := s .codes [w ]; ! inUse {
230+ // Reserve this candidate by advancing the cursor; actual insertion happens in CreateCode.
231+ s .singleIdx = (s .singleIdx + tried + 1 ) % n
232+ return w , true
233+ }
234+ }
235+ return "" , false
236+ }
237+
238+ // pickPairLocked returns an unused two-word code "w1-w2" if available.
239+ // Requires s.mu to be held (write lock) because it advances s.pairA/B.
240+ func (s * Store ) pickPairLocked () (string , bool ) {
241+ n := len (s .words )
242+ if n == 0 {
243+ return "" , false
244+ }
245+ if len (s .perm ) != n {
246+ s .perm = make ([]int , n )
247+ for i := range s .perm {
248+ s .perm [i ] = i
249+ }
250+ }
251+ // Scan n*n pairs in a rotating manner.
252+ for scanned := 0 ; scanned < n * n ; scanned ++ {
253+ a := s.words [s.perm [s.pairA ]]
254+ b := s.words [s.perm [s.pairB ]]
255+ code := a + "-" + b
256+
257+ // Move pairB then roll into pairA
258+ s .pairB ++
259+ if s .pairB >= n {
260+ s .pairB = 0
261+ s .pairA = (s .pairA + 1 ) % n
262+ }
263+
264+ if _ , inUse := s .codes [code ]; ! inUse {
265+ return code , true
266+ }
267+ }
268+ return "" , false
269+ }
270+
271+ // generateLegacyUnlocked reproduces the old style code (4 words + 4 digits).
272+ // Requires s.mu to be held (write lock) only because we share randInt; it doesn't mutate s.
273+ func (s * Store ) generateLegacyUnlocked () (string , error ) {
180274 const wordsPerCode = 4
181275 if len (s .words ) < 2 {
182276 return "" , errors .New ("wordlist too small" )
@@ -194,7 +288,7 @@ func (s *Store) generateCode() (string, error) {
194288 return "" , err
195289 }
196290 parts = append (parts , fmt .Sprintf ("%04d" , d ))
197- return strings .Join (parts , "-" ), nil
291+ return strings .ToLower ( strings . Join (parts , "-" ) ), nil
198292}
199293
200294func randInt (n int ) (int , error ) {
0 commit comments