mirror of
https://github.com/yusing/godoxy.git
synced 2026-03-14 06:17:08 +01:00
365 lines
6.9 KiB
Markdown
365 lines
6.9 KiB
Markdown
# JSON Store
|
|
|
|
The jsonstore package provides persistent JSON storage with namespace support, using thread-safe concurrent maps and automatic loading/saving.
|
|
|
|
## Overview
|
|
|
|
The jsonstore package implements a simple yet powerful JSON storage system for GoDoxy, supporting both key-value stores (MapStore) and single object stores (ObjectStore) with automatic persistence to JSON files.
|
|
|
|
### Key Features
|
|
|
|
- Namespace-based storage
|
|
- Thread-safe concurrent map operations (xsync)
|
|
- Automatic JSON loading on initialization
|
|
- Automatic JSON saving on program exit
|
|
- Generic type support
|
|
- Marshal/Unmarshal integration
|
|
|
|
## Architecture
|
|
|
|
```mermaid
|
|
graph TD
|
|
A[JSON Store] --> B{Namespace}
|
|
B --> C[MapStore]
|
|
B --> D[ObjectStore]
|
|
|
|
C --> E[xsync.Map]
|
|
D --> F[Single Object]
|
|
|
|
G[Storage File] --> H[Load on Init]
|
|
H --> I[Parse JSON]
|
|
I --> J[xsync.Map or Object]
|
|
|
|
K[Program Exit] --> L[Save All]
|
|
L --> M[Serialize to JSON]
|
|
M --> N[Write Files]
|
|
```
|
|
|
|
## Core Components
|
|
|
|
### MapStore
|
|
|
|
```go
|
|
type MapStore[VT any] struct {
|
|
*xsync.Map[string, VT]
|
|
}
|
|
|
|
// Implements:
|
|
// - Initialize() - initializes the internal map
|
|
// - MarshalJSON() - serializes to JSON
|
|
// - UnmarshalJSON() - deserializes from JSON
|
|
```
|
|
|
|
### ObjectStore
|
|
|
|
```go
|
|
type ObjectStore[Pointer Initializer] struct {
|
|
ptr Pointer
|
|
}
|
|
|
|
// Initializer interface requires:
|
|
// - Initialize()
|
|
```
|
|
|
|
### Store Interface
|
|
|
|
```go
|
|
type store interface {
|
|
Initialize()
|
|
json.Marshaler
|
|
json.Unmarshaler
|
|
}
|
|
```
|
|
|
|
## Public API
|
|
|
|
### MapStore Creation
|
|
|
|
```go
|
|
// Store creates a new namespace map store.
|
|
func Store[VT any](namespace namespace) MapStore[VT]
|
|
```
|
|
|
|
### ObjectStore Creation
|
|
|
|
```go
|
|
// Object creates a new namespace object store.
|
|
func Object[Ptr Initializer](namespace namespace) Ptr
|
|
```
|
|
|
|
## Usage
|
|
|
|
### MapStore Example
|
|
|
|
```go
|
|
// Define a namespace
|
|
type UserID string
|
|
|
|
// Create a store for user sessions
|
|
var sessions = jsonstore.Store[UserID]("sessions")
|
|
|
|
// Store a value
|
|
sessions.Store("user123", "session-token-abc")
|
|
|
|
// Load a value
|
|
token, ok := sessions.Load("user123")
|
|
if ok {
|
|
fmt.Println("Session:", token)
|
|
}
|
|
|
|
// Iterate over all entries
|
|
for id, token := range sessions.Range {
|
|
fmt.Printf("%s: %s\n", id, token)
|
|
}
|
|
|
|
// Delete a value
|
|
sessions.Delete("user123")
|
|
```
|
|
|
|
### ObjectStore Example
|
|
|
|
```go
|
|
// Define a struct that implements Initialize
|
|
type AppConfig struct {
|
|
Name string
|
|
Version int
|
|
}
|
|
|
|
func (c *AppConfig) Initialize() {
|
|
c.Name = "MyApp"
|
|
c.Version = 1
|
|
}
|
|
|
|
// Create an object store
|
|
var config = jsonstore.Object[*AppConfig]("app_config")
|
|
|
|
// Access the object
|
|
fmt.Printf("App: %s v%d\n", config.Name, config.Version)
|
|
|
|
// Modify and save (automatic on exit)
|
|
config.Version = 2
|
|
```
|
|
|
|
### Complete Example
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"github.com/yusing/godoxy/internal/jsonstore"
|
|
)
|
|
|
|
type Settings struct {
|
|
Theme string
|
|
Lang string
|
|
}
|
|
|
|
func (s *Settings) Initialize() {
|
|
s.Theme = "dark"
|
|
s.Lang = "en"
|
|
}
|
|
|
|
func main() {
|
|
// Create namespace type
|
|
type SettingsKey string
|
|
|
|
// Create stores
|
|
var settings = jsonstore.Object[*Settings]("settings")
|
|
var cache = jsonstore.Store[string]("cache")
|
|
|
|
// Use stores
|
|
settings.Theme = "light"
|
|
cache.Store("key1", "value1")
|
|
|
|
// On program exit, all stores are automatically saved
|
|
}
|
|
```
|
|
|
|
## Data Flow
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Application
|
|
participant Store
|
|
participant xsync.Map
|
|
participant File
|
|
|
|
Application->>Store: Store(key, value)
|
|
Store->>xsync.Map: Store(key, value)
|
|
xsync.Map-->>Store: Done
|
|
|
|
Application->>Store: Load(key)
|
|
Store->>xsync.Map: Load(key)
|
|
xsync.Map-->>Store: value
|
|
Store-->>Application: value
|
|
|
|
Application->>Store: Save()
|
|
Store->>File: Marshal JSON
|
|
File-->>Store: Success
|
|
|
|
Note over Store,File: On program exit
|
|
Store->>File: Save all stores
|
|
File-->>Store: Complete
|
|
```
|
|
|
|
## Namespace
|
|
|
|
Namespaces are string identifiers for different storage areas:
|
|
|
|
```go
|
|
type namespace string
|
|
|
|
// Create namespaces
|
|
var (
|
|
users = jsonstore.Store[User]("users")
|
|
sessions = jsonstore.Store[Session]("sessions")
|
|
config = jsonstore.Object[*Config]("config")
|
|
metadata = jsonstore.Store[string]("metadata")
|
|
)
|
|
```
|
|
|
|
### Reserved Names
|
|
|
|
None
|
|
|
|
## File Storage
|
|
|
|
### File Location
|
|
|
|
```go
|
|
var storesPath = common.DataDir // Typically ./data/.{namespace}.json
|
|
```
|
|
|
|
### File Format
|
|
|
|
Stores are saved as `{namespace}.json`:
|
|
|
|
```json
|
|
{
|
|
"key1": "value1",
|
|
"key2": "value2"
|
|
}
|
|
```
|
|
|
|
### Automatic Loading
|
|
|
|
On initialization, stores are loaded from disk:
|
|
|
|
```go
|
|
func loadNS[T store](ns namespace) T {
|
|
store := reflect.New(reflect.TypeFor[T]().Elem()).Interface().(T)
|
|
store.Initialize()
|
|
|
|
path := filepath.Join(storesPath, string(ns)+".json")
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
log.Err(err).Msg("failed to load store")
|
|
}
|
|
return store
|
|
}
|
|
defer file.Close()
|
|
|
|
if err := sonic.ConfigDefault.NewDecoder(file).Decode(&store); err != nil {
|
|
log.Err(err).Msg("failed to decode store")
|
|
}
|
|
|
|
stores[ns] = store
|
|
return store
|
|
}
|
|
```
|
|
|
|
### Automatic Saving
|
|
|
|
On program exit, all stores are saved:
|
|
|
|
```go
|
|
func init() {
|
|
task.OnProgramExit("save_stores", func() {
|
|
if err := save(); err != nil {
|
|
log.Error().Err(err).Msg("failed to save stores")
|
|
}
|
|
})
|
|
}
|
|
|
|
func save() error {
|
|
for ns, store := range stores {
|
|
path := filepath.Join(storesPath, string(ns)+".json")
|
|
if err := serialization.SaveJSON(path, &store, 0644); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
```
|
|
|
|
## Thread Safety
|
|
|
|
The MapStore uses `xsync.Map` for thread-safe operations:
|
|
|
|
```go
|
|
type MapStore[VT any] struct {
|
|
*xsync.Map[string, VT]
|
|
}
|
|
|
|
// All operations are safe:
|
|
// - Load, Store, Delete
|
|
// - Range iteration
|
|
// - LoadAndDelete
|
|
// - LoadOrCompute
|
|
```
|
|
|
|
## JSON Serialization
|
|
|
|
### MarshalJSON
|
|
|
|
```go
|
|
func (s MapStore[VT]) MarshalJSON() ([]byte, error) {
|
|
return sonic.Marshal(xsync.ToPlainMap(s.Map))
|
|
}
|
|
```
|
|
|
|
### UnmarshalJSON
|
|
|
|
```go
|
|
func (s *MapStore[VT]) UnmarshalJSON(data []byte) error {
|
|
tmp := make(map[string]VT)
|
|
if err := sonic.Unmarshal(data, &tmp); err != nil {
|
|
return err
|
|
}
|
|
s.Map = xsync.NewMap[string, VT](xsync.WithPresize(len(tmp)))
|
|
for k, v := range tmp {
|
|
s.Store(k, v)
|
|
}
|
|
return nil
|
|
}
|
|
```
|
|
|
|
## Integration Points
|
|
|
|
The jsonstore package integrates with:
|
|
|
|
- **Serialization**: JSON marshaling/unmarshaling
|
|
- **Task Management**: Program exit callbacks
|
|
- **Common**: Data directory configuration
|
|
|
|
## Error Handling
|
|
|
|
Errors are logged but don't prevent store usage:
|
|
|
|
```go
|
|
if err := sonic.Unmarshal(data, &tmp); err != nil {
|
|
log.Err(err).
|
|
Str("path", path).
|
|
Msg("failed to load store")
|
|
}
|
|
```
|
|
|
|
## Performance Considerations
|
|
|
|
- Uses `xsync.Map` for lock-free reads
|
|
- Presizes maps based on input data
|
|
- Sonic library for fast JSON parsing
|
|
- Background save on program exit (non-blocking)
|