Skip to content

Commit 744eb63

Browse files
authored
feat: update storage adapter (#15)
* fix: remove file storage adapter * feat: add close method to the storage adapter
1 parent 87725d9 commit 744eb63

14 files changed

Lines changed: 255 additions & 277 deletions

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,9 +236,12 @@ type StorageAdapter interface {
236236
Save(events []Event) error
237237
Load() ([]Event, error)
238238
Clear() error
239+
Close() error
239240
}
240241
```
241242

243+
The `Close()` method is called when the client is disposed, allowing storage adapters to release resources (e.g., close database connections, Redis clients, file handles).
244+
242245
#### Logger Adapter
243246

244247
```go

README.md

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func main() {
6161
APIKey: "your-api-key",
6262
Endpoint: "https://api.example.com/events",
6363
HTTPAdapter: adapters.NewNetHTTPAdapter(),
64-
StorageAdapter: adapters.NewFileStorageAdapter("ripple_events.json"),
64+
StorageAdapter: adapters.NewNoOpStorageAdapter(),
6565
})
6666
if err != nil {
6767
panic(err)
@@ -129,7 +129,7 @@ Configuration validation:
129129

130130
### Client Methods
131131

132-
#### `Init() error`
132+
#### `Init()`
133133

134134
Initializes the client and restores persisted events. Uses double-checked locking for thread safety. Resets the disposed state, so calling `Init()` after `Dispose()` re-enables the client.
135135

@@ -253,19 +253,17 @@ client, err := ripple.NewClient(ripple.ClientConfig{
253253

254254
| Adapter | Capacity | Persistence | Use Case |
255255
| ---------------------- | --------- | ----------- | --------------------------------- |
256-
| **FileStorageAdapter** | Unlimited | Permanent | Default, persistent event storage |
257-
| **NoOpStorageAdapter** | N/A | None | When persistence is not needed |
256+
| **NoOpStorageAdapter** | N/A | None | Default, no persistence |
258257

259258
```go
260259
import "github.com/Tap30/ripple-go/adapters"
261260

262-
// Persistent storage
263-
fileStorage := adapters.NewFileStorageAdapter("ripple_events.json")
264-
265-
// No persistence
266-
noopStorage := adapters.NewNoOpStorageAdapter()
261+
// No persistence (default)
262+
storage := adapters.NewNoOpStorageAdapter()
267263
```
268264

265+
For custom storage implementations (e.g., file, Redis, database), implement the `StorageAdapter` interface. See [adapters/README.md](./adapters/README.md) for examples.
266+
269267
## Concurrency Guarantees
270268

271269
- **Thread-Safe Flush**: Multiple concurrent `Flush()` calls are serialized via mutex

adapters/README.md

Lines changed: 102 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,14 @@ type StorageAdapter interface {
3030
Save(events []Event) error
3131
Load() ([]Event, error)
3232
Clear() error
33+
Close() error
3334
}
3435
```
3536

36-
**Default Implementation:** `FileStorageAdapter`
37-
38-
- Stores events as JSON in a file
39-
- Suitable for server environments
40-
4137
**NoOp Implementation:** `NoOpStorageAdapter`
4238

4339
- Performs no storage operations
40+
- Default choice for most use cases
4441
- Useful when persistence is not required
4542

4643
### LoggerAdapter
@@ -100,13 +97,107 @@ func (a *MyHTTPAdapter) SendWithContext(ctx context.Context, endpoint string, ev
10097
```go
10198
package main
10299

103-
import "github.com/Tap30/ripple-go/adapters"
100+
import (
101+
"encoding/json"
102+
"os"
103+
"github.com/Tap30/ripple-go/adapters"
104+
)
105+
106+
type FileStorage struct {
107+
filepath string
108+
}
109+
110+
func (f *FileStorage) Save(events []adapters.Event) error {
111+
data, err := json.Marshal(events)
112+
if err != nil {
113+
return err
114+
}
115+
return os.WriteFile(f.filepath, data, 0o644)
116+
}
117+
118+
func (f *FileStorage) Load() ([]adapters.Event, error) {
119+
data, err := os.ReadFile(f.filepath)
120+
if err != nil {
121+
if os.IsNotExist(err) {
122+
return []adapters.Event{}, nil
123+
}
124+
return nil, err
125+
}
126+
var events []adapters.Event
127+
if err := json.Unmarshal(data, &events); err != nil {
128+
return nil, err
129+
}
130+
return events, nil
131+
}
132+
133+
func (f *FileStorage) Clear() error {
134+
err := os.Remove(f.filepath)
135+
if err != nil && !os.IsNotExist(err) {
136+
return err
137+
}
138+
return nil
139+
}
140+
141+
func (f *FileStorage) Close() error {
142+
// No persistent connections for file storage
143+
return nil
144+
}
145+
```
146+
147+
### Example: Redis Storage Adapter
148+
149+
```go
150+
package main
151+
152+
import (
153+
"context"
154+
"encoding/json"
155+
"github.com/Tap30/ripple-go/adapters"
156+
"github.com/redis/go-redis/v9"
157+
)
158+
159+
type RedisStorage struct {
160+
client *redis.Client
161+
key string
162+
}
163+
164+
func NewRedisStorage(addr, key string) *RedisStorage {
165+
return &RedisStorage{
166+
client: redis.NewClient(&redis.Options{Addr: addr}),
167+
key: key,
168+
}
169+
}
170+
171+
func (r *RedisStorage) Save(events []adapters.Event) error {
172+
data, err := json.Marshal(events)
173+
if err != nil {
174+
return err
175+
}
176+
return r.client.Set(context.Background(), r.key, data, 0).Err()
177+
}
178+
179+
func (r *RedisStorage) Load() ([]adapters.Event, error) {
180+
data, err := r.client.Get(context.Background(), r.key).Result()
181+
if err == redis.Nil {
182+
return []adapters.Event{}, nil
183+
}
184+
if err != nil {
185+
return nil, err
186+
}
187+
var events []adapters.Event
188+
if err := json.Unmarshal([]byte(data), &events); err != nil {
189+
return nil, err
190+
}
191+
return events, nil
192+
}
104193

105-
type RedisStorage struct{}
194+
func (r *RedisStorage) Clear() error {
195+
return r.client.Del(context.Background(), r.key).Err()
196+
}
106197

107-
func (r *RedisStorage) Save(events []adapters.Event) error { return nil }
108-
func (r *RedisStorage) Load() ([]adapters.Event, error) { return nil, nil }
109-
func (r *RedisStorage) Clear() error { return nil }
198+
func (r *RedisStorage) Close() error {
199+
return r.client.Close()
200+
}
110201
```
111202

112203
## Usage with Client
@@ -126,7 +217,7 @@ func main() {
126217
APIKey: "your-api-key",
127218
Endpoint: "https://api.example.com/events",
128219
HTTPAdapter: adapters.NewNetHTTPAdapter(),
129-
StorageAdapter: adapters.NewFileStorageAdapter("ripple_events.json"),
220+
StorageAdapter: adapters.NewNoOpStorageAdapter(),
130221
LoggerAdapter: adapters.NewPrintLoggerAdapter(adapters.LogLevelDebug),
131222
})
132223
if err != nil {

adapters/file_storage_adapter_test.go

Lines changed: 0 additions & 132 deletions
This file was deleted.

adapters/noop_storage_adapter.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,8 @@ func (n *NoOpStorageAdapter) Load() ([]Event, error) {
2323
func (n *NoOpStorageAdapter) Clear() error {
2424
return nil
2525
}
26+
27+
// Close does nothing and always returns nil.
28+
func (n *NoOpStorageAdapter) Close() error {
29+
return nil
30+
}

adapters/noop_storage_adapter_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ func TestNoOpStorageAdapter_Clear(t *testing.T) {
4343
}
4444
}
4545

46+
func TestNoOpStorageAdapter_Close(t *testing.T) {
47+
adapter := NewNoOpStorageAdapter()
48+
49+
err := adapter.Close()
50+
if err != nil {
51+
t.Errorf("Close should always return nil, got: %v", err)
52+
}
53+
}
54+
4655
func TestNoOpStorageAdapter_Interface(t *testing.T) {
4756
var _ StorageAdapter = (*NoOpStorageAdapter)(nil)
4857
}

adapters/storage_adapter.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,10 @@ type StorageAdapter interface {
2020
//
2121
// Returns error if clear fails.
2222
Clear() error
23+
24+
// Close releases any resources held by the storage adapter.
25+
// Called when the client is disposed.
26+
//
27+
// Returns error if close fails.
28+
Close() error
2329
}

contract_validation_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,10 @@ func TestContractCompliance(t *testing.T) {
6767
client, _ := NewClient(createTestConfig())
6868
clientValue := reflect.ValueOf(client)
6969

70-
// Init() error
70+
// Init()
7171
initType := clientValue.MethodByName("Init").Type()
72-
if initType.NumIn() != 0 || initType.NumOut() != 1 {
73-
t.Error("Init should take no parameters and return error")
72+
if initType.NumIn() != 0 || initType.NumOut() != 0 {
73+
t.Error("Init should take no parameters and return nothing")
7474
}
7575

7676
// Track(string, map[string]any, map[string]any) error

dispatcher.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,12 @@ func (d *Dispatcher) Dispose() {
139139

140140
d.stopTimer()
141141
d.queue.Clear()
142+
143+
if err := d.storageAdapter.Close(); err != nil {
144+
d.loggerAdapter.Error("failed to close storage adapter", map[string]any{
145+
"error": err.Error(),
146+
})
147+
}
142148
}
143149

144150
// applyQueueLimit applies the maxBufferSize limit using FIFO eviction.

0 commit comments

Comments
 (0)