55 "errors"
66 "time"
77
8+ "github.com/candango/httpok/logger"
89 "github.com/candango/httpok/security"
910)
1011
@@ -45,32 +46,43 @@ type Store interface {
4546 // Stop tears down resources.
4647 Stop (ctx context.Context ) error
4748
49+ // RequiresPurge returns whether this Store implementation requires manual
50+ // session expiration cleanup. Stores with automatic TTL/expiration (e.g.,
51+ // Redis) return false. Stores tracking expiration manually (e.g.,
52+ // MemoryStore, FileStore) return true, signaling that StoreEngine must
53+ // periodically call Purge to remove expired sessions.
54+ //
55+ // Example implementations:
56+ // - MemoryStore.RequiresPurge() -> true (manual LastTouched tracking)
57+ // - FileStore.RequiresPurge() -> true (file mtime tracking)
58+ // - RedisStore.RequiresPurge() -> false (Redis TTL automatic)
59+ RequiresPurge () bool
60+
4861 // Touch updates the session's ttl, typically to implement sliding
4962 // expiration. It does not modify the session data.
5063 // Returns an error if the id does not exist.
5164 Touch (ctx context.Context , id string ) error
5265}
5366
67+ type storeEngineOptions func (* StoreEngine )
68+
5469// StoreEngine implements the Engine interface by delegating session operations
5570// to a pluggable Store backend. It holds engine properties and a Store
5671// instance, allowing flexible session storage strategies (e.g., in-memory,
5772// file, etc.).
5873type StoreEngine struct {
5974 properties * EngineProperties
6075 Store
76+ logger logger.Logger
77+ purgeDone chan struct {}
78+ started bool
6179}
6280
6381// NewStoreEngine creates and returns a new StoreEngine.
6482// If custom properties are provided, they are used; otherwise, default
6583// settings are applied.
66- func NewStoreEngine (store Store , props ... * EngineProperties ) * StoreEngine {
67- if len (props ) > 0 && props [0 ] != nil {
68- return & StoreEngine {
69- properties : props [0 ],
70- Store : store ,
71- }
72- }
73- return & StoreEngine {
84+ func NewStoreEngine (store Store , opts ... storeEngineOptions ) * StoreEngine {
85+ e := & StoreEngine {
7486 properties : & EngineProperties {
7587 AgeLimit : 30 * time .Minute ,
7688 Enabled : true ,
@@ -79,7 +91,24 @@ func NewStoreEngine(store Store, props ...*EngineProperties) *StoreEngine {
7991 Prefix : DefaultPrefix ,
8092 PurgeDuration : 2 * time .Minute ,
8193 },
82- Store : store ,
94+ Store : store ,
95+ logger : & logger.StandardLogger {},
96+ }
97+ for _ , opt := range opts {
98+ opt (e )
99+ }
100+ return e
101+ }
102+
103+ func WithLogger (l logger.Logger ) storeEngineOptions {
104+ return func (e * StoreEngine ) {
105+ e .logger = l
106+ }
107+ }
108+
109+ func WithProperties (p * EngineProperties ) storeEngineOptions {
110+ return func (e * StoreEngine ) {
111+ e .properties = p
83112 }
84113}
85114
@@ -91,6 +120,15 @@ func (e *StoreEngine) NewId(ctx context.Context) string {
91120
92121// Start initializes the engine with the given context.
93122func (e * StoreEngine ) Start (ctx context.Context ) error {
123+ if e .started {
124+ return errors .New ("store engine already started" )
125+ }
126+ e .started = true
127+ if e .RequiresPurge () {
128+ e .purgeDone = make (chan struct {})
129+ go e .periodicPurge (ctx )
130+ }
131+
94132 return e .Store .Start (ctx )
95133}
96134
@@ -105,6 +143,27 @@ func (e *StoreEngine) Properties() *EngineProperties {
105143 return e .properties
106144}
107145
146+ func (e * StoreEngine ) periodicPurge (ctx context.Context ) error {
147+ ticker := time .NewTicker (e .properties .PurgeDuration )
148+ defer ticker .Stop ()
149+
150+ for {
151+ select {
152+ case <- ticker .C :
153+ ticker .Stop ()
154+
155+ if err := e .Purge (ctx ); err != nil {
156+ e .logger .Errorf ("periodic purge failed: %v" , err )
157+ }
158+ ticker = time .NewTicker (e .properties .PurgeDuration )
159+ case <- e .purgeDone :
160+ return nil
161+ case <- ctx .Done ():
162+ return nil
163+ }
164+ }
165+ }
166+
108167// Purge removes expired or invalid sessions.
109168func (e * StoreEngine ) Purge (ctx context.Context ) error {
110169 if ! e .properties .Enabled {
@@ -120,7 +179,7 @@ func (e *StoreEngine) GetSession(ctx context.Context, id string) (Session, error
120179 return s , errors .New ("engine is disabled" )
121180 }
122181 if id == "" {
123- return s , errors .New ("engine is disabled " )
182+ return s , errors .New ("session id is empty " )
124183 }
125184 var v map [string ]any
126185 ok , err := e .Store .Exists (ctx , id )
@@ -138,7 +197,7 @@ func (e *StoreEngine) GetSession(ctx context.Context, id string) (Session, error
138197 if err != nil {
139198 return s , err
140199 }
141- e .Store .Touch (ctx , id )
200+ err = e .Store .Touch (ctx , id )
142201 if err != nil {
143202 return s , err
144203 }
@@ -170,7 +229,7 @@ func (e *StoreEngine) SaveSession(ctx context.Context, id string, session Sessio
170229 return errors .New ("engine is disabled" )
171230 }
172231 if id == "" {
173- return errors .New ("engine is disabled " )
232+ return errors .New ("session id is empty " )
174233 }
175234
176235 data , err := e .properties .Encoder .Encode (session .Data )
0 commit comments