Skip to main content
Glama

Storyden

by Southclaws
Mozilla Public License 2.0
227
repository.go5.19 kB
package settings import ( "context" "encoding/json" "log/slog" "sync" "time" "github.com/Southclaws/fault" "github.com/Southclaws/fault/fctx" "github.com/Southclaws/fault/fmsg" "github.com/puzpuzpuz/xsync/v4" "go.uber.org/fx" "github.com/Southclaws/storyden/internal/ent" "github.com/Southclaws/storyden/internal/utils/errutil" ) // StorydenPrimarySettingsKey is the key used to store the primary settings data // in the database. The settings table itself can contain other settings and is // treated as a key-value store. Storyden itself only cares about the row with // this key, other rows may be used by plugins or any other integrated systems. const StorydenPrimarySettingsKey = "storyden_system" type SettingsRepository struct { logger *slog.Logger db *ent.Client // cached stores the most recent copy of all the settings from the database. // Directly changing settings via external database queries will result in // settings not immediately updating so it's advised to always go via API. cachedSettings *xsync.Map[string, any] // mutex protects access to cacheLastFetch cacheMu sync.RWMutex cacheLastFetch time.Time } func New(ctx context.Context, lc fx.Lifecycle, logger *slog.Logger, db *ent.Client) (*SettingsRepository, error) { d := &SettingsRepository{ logger: logger, db: db, cachedSettings: xsync.NewMap[string, any](), } lc.Append(fx.StartHook(func() error { if err := d.initDefaults(ctx); err != nil { return fault.Wrap(err, fctx.With(ctx), fmsg.With("failed to initialise default settings")) } return nil })) return d, nil } // initDefaults is one of the only SettingsRepository writes that happens on first boot. It sets // up some basic configuration settings for a brand new empty installation. func (d *SettingsRepository) initDefaults(ctx context.Context) error { _, err := d.db.Setting.Get(ctx, StorydenPrimarySettingsKey) if ent.IsNotFound(err) { _, err = d.setDefaults(ctx) } if err != nil { return fault.Wrap(err, fctx.With(ctx)) } return nil } func (d *SettingsRepository) Get(ctx context.Context) (*Settings, error) { s, ok := d.tryCached() if ok { go d.recache(ctx) return s, nil } settings, err := d.get(ctx) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } d.cache(settings) return settings, nil } // Set will merge a partial update into the current settings and save new data. func (d *SettingsRepository) Set(ctx context.Context, s Settings) (*Settings, error) { current, err := d.Get(ctx) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } err = current.Merge(s) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } b, err := json.Marshal(current) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } r, err := d.db.Setting. UpdateOneID(StorydenPrimarySettingsKey). SetValue(string(b)). Save(ctx) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } settings, err := mapSettings(r) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } d.cache(settings) return settings, nil } func (d *SettingsRepository) setDefaults(ctx context.Context) (*Settings, error) { b, err := json.Marshal(DefaultSettings) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } s, err := d.db.Setting.Create(). SetID(StorydenPrimarySettingsKey). SetValue(string(b)). Save(ctx) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } return mapSettings(s) } func (d *SettingsRepository) get(ctx context.Context) (*Settings, error) { r, err := d.db.Setting.Get(ctx, StorydenPrimarySettingsKey) if ent.IsNotFound(err) { // Ensure defaults are written to the database if they don't exist. // This should only happen in tests where initDefaults isn't called. return d.setDefaults(ctx) } if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } settings, err := mapSettings(r) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } return settings, nil } func (d *SettingsRepository) tryCached() (*Settings, bool) { v, ok := d.cachedSettings.Load(StorydenPrimarySettingsKey) if !ok { return nil, false } s, ok := v.(*Settings) if !ok { return nil, false } return s, true } func (d *SettingsRepository) cache(s *Settings) { d.cachedSettings.Store(StorydenPrimarySettingsKey, s) d.cacheMu.Lock() d.cacheLastFetch = time.Now() d.cacheMu.Unlock() } func (d *SettingsRepository) recache(ctx context.Context) { d.cacheMu.RLock() timeSinceLastFetch := time.Since(d.cacheLastFetch) d.cacheMu.RUnlock() if timeSinceLastFetch < 5*time.Minute { return } settings, err := d.get(ctx) if err != nil { if errutil.IsIgnored(err) { return } d.logger.Error("failed to recache settings", slog.String("error", err.Error())) return } // NOTE: There's a small chance of stale data here if an update occurs since // recache was called (via goroutine) but before the cache is updated. This // should be resolved at some point via a database key staleness timestamp. d.cache(settings) } // NOTE: There's currently no way to reset/delete or work with non-system data.

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Southclaws/storyden'

If you have feedback or need assistance with the MCP directory API, please join our Discord server