@@ -22,15 +22,19 @@ 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 
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 
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+ 
31+ 	// The following fields are kept for backward-compat constructors but unused 
32+ 	// now that codes are PINs. Safe to remove later if desired. 
33+ 	words      []string 
34+ 	perm       []int 
35+ 	singleIdx  int 
36+ 	pairA      int 
37+ 	pairB      int 
3438}
3539
3640func  NewStore (words  []string , ttl  time.Duration , pregenCap  int ) * Store  {
@@ -43,19 +47,6 @@ func NewStore(words []string, ttl time.Duration, pregenCap int) *Store {
4347		ttl :       ttl ,
4448		words :     words ,
4549	}
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- 	}
5950	if  pregenCap  >  0  {
6051		s .pregen  =  make (chan  string , pregenCap )
6152		go  s .pregenerator ()
@@ -139,6 +130,7 @@ func (s *Store) pregenerator() {
139130		}
140131		code , err  :=  s .newUniqueCode ()
141132		if  err  !=  nil  {
133+ 			// If space is temporarily exhausted, back off a bit. 
142134			time .Sleep (20  *  time .Millisecond )
143135			continue 
144136		}
@@ -166,6 +158,7 @@ func (s *Store) takeCodeFromPool() (string, bool) {
166158}
167159
168160func  (s  * Store ) newUniqueCode () (string , error ) {
161+ 	// Fast attempts first. 
169162	for  tries  :=  0 ; tries  <  8 ; tries ++  {
170163		c , err  :=  s .generateCode ()
171164		if  err  !=  nil  {
@@ -178,6 +171,7 @@ func (s *Store) newUniqueCode() (string, error) {
178171			return  c , nil 
179172		}
180173	}
174+ 	// Keep trying until we find a free PIN (or space is exhausted). 
181175	for  {
182176		c , err  :=  s .generateCode ()
183177		if  err  !=  nil  {
@@ -192,103 +186,39 @@ func (s *Store) newUniqueCode() (string, error) {
192186	}
193187}
194188
189+ // generateCode now produces a unique 4-digit PIN "0000".."9999". 
195190func  (s  * Store ) generateCode () (string , error ) {
196- 	// We need to see what's currently allocated, and advance our fair cursors. 
191+ 	const  maxPins  =  10000 
192+ 
197193	s .mu .Lock ()
198194	defer  s .mu .Unlock ()
199195
200- 	// 1) Try single-word pool . 
201- 	if  code ,  ok   :=   s . pickSingleLocked ();  ok  {
202- 		return  code ,  nil 
196+ 	// If the entire PIN space is allocated, bail out . 
197+ 	if  len ( s . codes )  >=   maxPins  {
198+ 		return  "" ,  errors . New ( "PIN space exhausted" ) 
203199	}
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- }
211200
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 
201+ 	// Try a handful of random picks first. 
202+ 	for  tries  :=  0 ; tries  <  16 ; tries ++  {
203+ 		n , err  :=  randInt (maxPins )
204+ 		if  err  !=  nil  {
205+ 			return  "" , err 
224206		}
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 
207+ 		pin  :=  fmt .Sprintf ("%04d" , n )
208+ 		if  _ , inUse  :=  s .codes [pin ]; ! inUse  {
209+ 			return  pin , nil 
233210		}
234211	}
235- 	return  "" , false 
236- }
237212
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 
213+ 	// Fallback: linear scan to find any free PIN. 
214+ 	for  i  :=  0 ; i  <  maxPins ; i ++  {
215+ 		pin  :=  fmt .Sprintf ("%04d" , i )
216+ 		if  _ , inUse  :=  s .codes [pin ]; ! inUse  {
217+ 			return  pin , nil 
249218		}
250219	}
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- }
270220
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 ) {
274- 	const  wordsPerCode  =  4 
275- 	if  len (s .words ) <  2  {
276- 		return  "" , errors .New ("wordlist too small" )
277- 	}
278- 	parts  :=  make ([]string , 0 , wordsPerCode + 1 )
279- 	for  i  :=  0 ; i  <  wordsPerCode ; i ++  {
280- 		idx , err  :=  randInt (len (s .words ))
281- 		if  err  !=  nil  {
282- 			return  "" , err 
283- 		}
284- 		parts  =  append (parts , s .words [idx ])
285- 	}
286- 	d , err  :=  randInt (10000 ) // 0000..9999 
287- 	if  err  !=  nil  {
288- 		return  "" , err 
289- 	}
290- 	parts  =  append (parts , fmt .Sprintf ("%04d" , d ))
291- 	return  strings .ToLower (strings .Join (parts , "-" )), nil 
221+ 	return  "" , errors .New ("PIN space exhausted" )
292222}
293223
294224func  randInt (n  int ) (int , error ) {
0 commit comments