refactor: tighten store AX documentation
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
cdf3124a40
commit
72eff0d164
6 changed files with 40 additions and 51 deletions
14
compact.go
14
compact.go
|
|
@ -12,12 +12,10 @@ import (
|
|||
|
||||
var defaultArchiveOutputDirectory = ".core/archive/"
|
||||
|
||||
// CompactOptions archives completed journal rows before a cutoff time to a
|
||||
// compressed JSONL file.
|
||||
//
|
||||
// Usage example: `options := store.CompactOptions{Before: time.Date(2026, 3, 30, 0, 0, 0, 0, time.UTC), Output: "/tmp/archive", Format: "gzip"}`
|
||||
// The default output directory is `.core/archive/`; the default format is
|
||||
// `gzip`, and `zstd` is also supported.
|
||||
// Usage example: `result := storeInstance.Compact(store.CompactOptions{Before: time.Now().Add(-90 * 24 * time.Hour)})`
|
||||
// Leave `Output` empty to write gzip JSONL archives under `.core/archive/`, or
|
||||
// set `Format` to `zstd` when downstream tooling expects `.jsonl.zst`.
|
||||
type CompactOptions struct {
|
||||
// Usage example: `options := store.CompactOptions{Before: time.Now().Add(-90 * 24 * time.Hour)}`
|
||||
Before time.Time
|
||||
|
|
@ -32,7 +30,7 @@ func (compactOptions CompactOptions) Normalised() CompactOptions {
|
|||
if compactOptions.Output == "" {
|
||||
compactOptions.Output = defaultArchiveOutputDirectory
|
||||
}
|
||||
compactOptions.Format = lowerText(core.Trim(compactOptions.Format))
|
||||
compactOptions.Format = lowercaseText(core.Trim(compactOptions.Format))
|
||||
if compactOptions.Format == "" {
|
||||
compactOptions.Format = "gzip"
|
||||
}
|
||||
|
|
@ -48,7 +46,7 @@ func (compactOptions CompactOptions) Validate() error {
|
|||
nil,
|
||||
)
|
||||
}
|
||||
switch lowerText(core.Trim(compactOptions.Format)) {
|
||||
switch lowercaseText(core.Trim(compactOptions.Format)) {
|
||||
case "", "gzip", "zstd":
|
||||
return nil
|
||||
default:
|
||||
|
|
@ -60,7 +58,7 @@ func (compactOptions CompactOptions) Validate() error {
|
|||
}
|
||||
}
|
||||
|
||||
func lowerText(text string) string {
|
||||
func lowercaseText(text string) string {
|
||||
builder := core.NewBuilder()
|
||||
for _, r := range text {
|
||||
builder.WriteRune(unicode.ToLower(r))
|
||||
|
|
|
|||
|
|
@ -323,7 +323,7 @@ func parseFluxTime(value string) (time.Time, error) {
|
|||
if value == "" {
|
||||
return time.Time{}, core.E("store.parseFluxTime", "range value is empty", nil)
|
||||
}
|
||||
value = firstOrEmptyString(core.Split(value, ","))
|
||||
value = firstStringOrEmpty(core.Split(value, ","))
|
||||
value = core.Trim(value)
|
||||
if core.HasPrefix(value, "time(v:") && core.HasSuffix(value, ")") {
|
||||
value = core.Trim(core.TrimSuffix(core.TrimPrefix(value, "time(v:"), ")"))
|
||||
|
|
|
|||
35
scope.go
35
scope.go
|
|
@ -60,10 +60,9 @@ func (scopedConfig ScopedStoreConfig) Validate() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ScopedStore prefixes group names with namespace + ":" before delegating to Store.
|
||||
// Usage example: `scopedStore, err := store.NewScoped(storeInstance, "tenant-a"); if err != nil { return }; if err := scopedStore.Set("colour", "blue"); err != nil { return }`
|
||||
//
|
||||
// Usage example: `scopedStore, err := store.NewScoped(storeInstance, "tenant-a"); if err != nil { return }; if err := scopedStore.SetIn("config", "colour", "blue"); err != nil { return }`
|
||||
// Usage example: `scopedStore, err := store.NewScoped(storeInstance, "tenant-a"); if err != nil { return }`
|
||||
// Usage example: `if err := scopedStore.Set("colour", "blue"); err != nil { return } // writes tenant-a:default/colour`
|
||||
// Usage example: `if err := scopedStore.SetIn("config", "colour", "blue"); err != nil { return } // writes tenant-a:config/colour`
|
||||
type ScopedStore struct {
|
||||
store *Store
|
||||
namespace string
|
||||
|
|
@ -82,10 +81,9 @@ type scopedWatcherBridge struct {
|
|||
done chan struct{}
|
||||
}
|
||||
|
||||
// NewScoped validates a namespace and prefixes groups with namespace + ":".
|
||||
// Usage example: `scopedStore, err := store.NewScoped(storeInstance, "tenant-a"); if err != nil { return }`
|
||||
// Prefer `NewScopedConfigured` when the namespace and quota are already known
|
||||
// as a struct literal.
|
||||
// Prefer `NewScopedConfigured(storeInstance, store.ScopedStoreConfig{Namespace: "tenant-a"})`
|
||||
// when the namespace and quota are already known at the call site.
|
||||
func NewScoped(storeInstance *Store, namespace string) (*ScopedStore, error) {
|
||||
if storeInstance == nil {
|
||||
return nil, core.E("store.NewScoped", "store instance is nil", nil)
|
||||
|
|
@ -101,8 +99,9 @@ func NewScoped(storeInstance *Store, namespace string) (*ScopedStore, error) {
|
|||
return scopedStore, nil
|
||||
}
|
||||
|
||||
// NewScopedConfigured validates the namespace and optional quota settings before constructing a ScopedStore.
|
||||
// Usage example: `scopedStore, err := store.NewScopedConfigured(storeInstance, store.ScopedStoreConfig{Namespace: "tenant-a", Quota: store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}}); if err != nil { return }`
|
||||
// This keeps the namespace and quota in one declarative literal instead of an
|
||||
// option chain.
|
||||
func NewScopedConfigured(storeInstance *Store, scopedConfig ScopedStoreConfig) (*ScopedStore, error) {
|
||||
if storeInstance == nil {
|
||||
return nil, core.E("store.NewScopedConfigured", "store instance is nil", nil)
|
||||
|
|
@ -291,7 +290,7 @@ func (scopedStore *ScopedStore) CountAll(groupPrefix ...string) (int, error) {
|
|||
if err := scopedStore.ensureReady("store.CountAll"); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return scopedStore.store.CountAll(scopedStore.namespacedGroup(firstOrEmptyString(groupPrefix)))
|
||||
return scopedStore.store.CountAll(scopedStore.namespacedGroup(firstStringOrEmpty(groupPrefix)))
|
||||
}
|
||||
|
||||
// Usage example: `groupNames, err := scopedStore.Groups("config")`
|
||||
|
|
@ -300,7 +299,7 @@ func (scopedStore *ScopedStore) Groups(groupPrefix ...string) ([]string, error)
|
|||
if err := scopedStore.ensureReady("store.Groups"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
groupNames, err := scopedStore.store.Groups(scopedStore.namespacedGroup(firstOrEmptyString(groupPrefix)))
|
||||
groupNames, err := scopedStore.store.Groups(scopedStore.namespacedGroup(firstStringOrEmpty(groupPrefix)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -319,7 +318,7 @@ func (scopedStore *ScopedStore) GroupsSeq(groupPrefix ...string) iter.Seq2[strin
|
|||
return
|
||||
}
|
||||
namespacePrefix := scopedStore.namespacePrefix()
|
||||
for groupName, err := range scopedStore.store.GroupsSeq(scopedStore.namespacedGroup(firstOrEmptyString(groupPrefix))) {
|
||||
for groupName, err := range scopedStore.store.GroupsSeq(scopedStore.namespacedGroup(firstStringOrEmpty(groupPrefix))) {
|
||||
if err != nil {
|
||||
if !yield("", err) {
|
||||
return
|
||||
|
|
@ -372,8 +371,8 @@ func (scopedStore *ScopedStore) PurgeExpired() (int64, error) {
|
|||
|
||||
// Usage example: `events := scopedStore.Watch("config")`
|
||||
// Usage example: `events := scopedStore.Watch("*")`
|
||||
// The returned events always use namespace-local group names, so a write to
|
||||
// `tenant-a:config` is delivered as `config`.
|
||||
// A write to `tenant-a:config` is delivered back to this scoped watcher as
|
||||
// `config`, so callers never have to strip the namespace themselves.
|
||||
func (scopedStore *ScopedStore) Watch(group string) <-chan Event {
|
||||
if scopedStore == nil || scopedStore.store == nil {
|
||||
return closedEventChannel()
|
||||
|
|
@ -479,8 +478,8 @@ func (scopedStore *ScopedStore) localiseWatchedEvent(event Event) (Event, bool)
|
|||
}
|
||||
|
||||
// Usage example: `unregister := scopedStore.OnChange(func(event store.Event) { fmt.Println(event.Group, event.Key, event.Value) })`
|
||||
// The callback receives the namespace-local group name, so a write to
|
||||
// `tenant-a:config` is reported as `config`.
|
||||
// A callback registered on `tenant-a` receives `config` rather than
|
||||
// `tenant-a:config`.
|
||||
func (scopedStore *ScopedStore) OnChange(callback func(Event)) func() {
|
||||
if scopedStore == nil || callback == nil {
|
||||
return func() {}
|
||||
|
|
@ -668,7 +667,7 @@ func (scopedStoreTransaction *ScopedStoreTransaction) CountAll(groupPrefix ...st
|
|||
if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.CountAll"); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return scopedStoreTransaction.storeTransaction.CountAll(scopedStoreTransaction.scopedStore.namespacedGroup(firstOrEmptyString(groupPrefix)))
|
||||
return scopedStoreTransaction.storeTransaction.CountAll(scopedStoreTransaction.scopedStore.namespacedGroup(firstStringOrEmpty(groupPrefix)))
|
||||
}
|
||||
|
||||
// Usage example: `groupNames, err := scopedStoreTransaction.Groups("config")`
|
||||
|
|
@ -678,7 +677,7 @@ func (scopedStoreTransaction *ScopedStoreTransaction) Groups(groupPrefix ...stri
|
|||
return nil, err
|
||||
}
|
||||
|
||||
groupNames, err := scopedStoreTransaction.storeTransaction.Groups(scopedStoreTransaction.scopedStore.namespacedGroup(firstOrEmptyString(groupPrefix)))
|
||||
groupNames, err := scopedStoreTransaction.storeTransaction.Groups(scopedStoreTransaction.scopedStore.namespacedGroup(firstStringOrEmpty(groupPrefix)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -698,7 +697,7 @@ func (scopedStoreTransaction *ScopedStoreTransaction) GroupsSeq(groupPrefix ...s
|
|||
}
|
||||
|
||||
namespacePrefix := scopedStoreTransaction.scopedStore.namespacePrefix()
|
||||
for groupName, err := range scopedStoreTransaction.storeTransaction.GroupsSeq(scopedStoreTransaction.scopedStore.namespacedGroup(firstOrEmptyString(groupPrefix))) {
|
||||
for groupName, err := range scopedStoreTransaction.storeTransaction.GroupsSeq(scopedStoreTransaction.scopedStore.namespacedGroup(firstStringOrEmpty(groupPrefix))) {
|
||||
if err != nil {
|
||||
if !yield("", err) {
|
||||
return
|
||||
|
|
|
|||
4
store.go
4
store.go
|
|
@ -763,7 +763,7 @@ func (storeInstance *Store) Groups(groupPrefix ...string) ([]string, error) {
|
|||
// Usage example: `for tenantGroupName, err := range storeInstance.GroupsSeq("tenant-a:") { if err != nil { break }; fmt.Println(tenantGroupName) }`
|
||||
// Usage example: `for groupName, err := range storeInstance.GroupsSeq() { if err != nil { break }; fmt.Println(groupName) }`
|
||||
func (storeInstance *Store) GroupsSeq(groupPrefix ...string) iter.Seq2[string, error] {
|
||||
actualGroupPrefix := firstOrEmptyString(groupPrefix)
|
||||
actualGroupPrefix := firstStringOrEmpty(groupPrefix)
|
||||
return func(yield func(string, error) bool) {
|
||||
if err := storeInstance.ensureReady("store.GroupsSeq"); err != nil {
|
||||
yield("", err)
|
||||
|
|
@ -808,7 +808,7 @@ func (storeInstance *Store) GroupsSeq(groupPrefix ...string) iter.Seq2[string, e
|
|||
}
|
||||
}
|
||||
|
||||
func firstOrEmptyString(values []string) string {
|
||||
func firstStringOrEmpty(values []string) string {
|
||||
if len(values) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -386,7 +386,7 @@ func (storeTransaction *StoreTransaction) Groups(groupPrefix ...string) ([]strin
|
|||
// Usage example: `for groupName, err := range transaction.GroupsSeq("tenant-a:") { if err != nil { break }; fmt.Println(groupName) }`
|
||||
// Usage example: `for groupName, err := range transaction.GroupsSeq() { if err != nil { break }; fmt.Println(groupName) }`
|
||||
func (storeTransaction *StoreTransaction) GroupsSeq(groupPrefix ...string) iter.Seq2[string, error] {
|
||||
actualGroupPrefix := firstOrEmptyString(groupPrefix)
|
||||
actualGroupPrefix := firstStringOrEmpty(groupPrefix)
|
||||
return func(yield func(string, error) bool) {
|
||||
if err := storeTransaction.ensureReady("store.Transaction.GroupsSeq"); err != nil {
|
||||
yield("", err)
|
||||
|
|
|
|||
34
workspace.go
34
workspace.go
|
|
@ -33,12 +33,11 @@ FROM workspace_entries`
|
|||
|
||||
var defaultWorkspaceStateDirectory = ".core/state/"
|
||||
|
||||
// Workspace keeps mutable work-in-progress in a SQLite file such as
|
||||
// `.core/state/scroll-session.duckdb` until Commit() or Discard() removes it.
|
||||
//
|
||||
// Usage example: `workspace, err := storeInstance.NewWorkspace("scroll-session"); if err != nil { return }; defer workspace.Discard()`
|
||||
//
|
||||
// Usage example: `workspace, err := storeInstance.NewWorkspace("scroll-session-2026-03-30"); if err != nil { return }; defer workspace.Discard(); _ = workspace.Put("like", map[string]any{"user": "@alice"})`
|
||||
// Each workspace keeps mutable work-in-progress in a SQLite file such as
|
||||
// `.core/state/scroll-session.duckdb` until `Commit()` or `Discard()` removes
|
||||
// it.
|
||||
type Workspace struct {
|
||||
name string
|
||||
store *Store
|
||||
|
|
@ -67,11 +66,10 @@ func (workspace *Workspace) DatabasePath() string {
|
|||
return workspace.databasePath
|
||||
}
|
||||
|
||||
// Close keeps the workspace file on disk so `RecoverOrphans(".core/state/")`
|
||||
// can reopen it later.
|
||||
//
|
||||
// Usage example: `if err := workspace.Close(); err != nil { return }`
|
||||
// Usage example: `if err := workspace.Close(); err != nil { return }; orphans := storeInstance.RecoverOrphans(".core/state"); _ = orphans`
|
||||
// `Close()` keeps the `.duckdb` file on disk so `RecoverOrphans(".core/state")`
|
||||
// can reopen it after a crash or interrupted agent run.
|
||||
func (workspace *Workspace) Close() error {
|
||||
return workspace.closeWithoutRemovingFiles()
|
||||
}
|
||||
|
|
@ -103,11 +101,9 @@ func (workspace *Workspace) ensureReady(operation string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// NewWorkspace opens a SQLite workspace file such as
|
||||
// `.core/state/scroll-session-2026-03-30.duckdb` and removes it when the
|
||||
// workspace is committed or discarded.
|
||||
//
|
||||
// Usage example: `workspace, err := storeInstance.NewWorkspace("scroll-session-2026-03-30"); if err != nil { return }; defer workspace.Discard()`
|
||||
// This creates `.core/state/scroll-session-2026-03-30.duckdb` by default and
|
||||
// removes it when the workspace is committed or discarded.
|
||||
func (storeInstance *Store) NewWorkspace(name string) (*Workspace, error) {
|
||||
if err := storeInstance.ensureReady("store.NewWorkspace"); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -218,11 +214,9 @@ func workspaceNameFromPath(stateDirectory, databasePath string) string {
|
|||
return core.TrimSuffix(relativePath, ".duckdb")
|
||||
}
|
||||
|
||||
// RecoverOrphans(".core/state") returns orphaned workspaces such as
|
||||
// `scroll-session-2026-03-30.duckdb` so callers can inspect Aggregate() and
|
||||
// choose Commit() or Discard().
|
||||
//
|
||||
// Usage example: `orphans := storeInstance.RecoverOrphans(".core/state"); for _, orphanWorkspace := range orphans { fmt.Println(orphanWorkspace.Name(), orphanWorkspace.Aggregate()) }`
|
||||
// This reopens leftover `.duckdb` files such as `scroll-session-2026-03-30`
|
||||
// so callers can inspect `Aggregate()` and choose `Commit()` or `Discard()`.
|
||||
func (storeInstance *Store) RecoverOrphans(stateDirectory string) []*Workspace {
|
||||
if storeInstance == nil {
|
||||
return nil
|
||||
|
|
@ -291,10 +285,9 @@ func (workspace *Workspace) Aggregate() map[string]any {
|
|||
return fields
|
||||
}
|
||||
|
||||
// Commit writes one completed workspace row to the journal and upserts the
|
||||
// summary entry in `workspace:NAME`.
|
||||
//
|
||||
// Usage example: `result := workspace.Commit(); if !result.OK { return }; fmt.Println(result.Value)`
|
||||
// `Commit()` writes one completed workspace row to the journal, upserts the
|
||||
// `workspace:NAME/summary` entry, and removes the workspace file.
|
||||
func (workspace *Workspace) Commit() core.Result {
|
||||
if err := workspace.ensureReady("store.Workspace.Commit"); err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
|
|
@ -321,10 +314,9 @@ func (workspace *Workspace) Discard() {
|
|||
_ = workspace.closeAndRemoveFiles()
|
||||
}
|
||||
|
||||
// Query runs SQL against the workspace buffer and returns rows as
|
||||
// `[]map[string]any` for ad-hoc inspection.
|
||||
//
|
||||
// Usage example: `result := workspace.Query("SELECT entry_kind, COUNT(*) AS count FROM workspace_entries GROUP BY entry_kind")`
|
||||
// `result.Value` contains `[]map[string]any`, which lets an agent inspect the
|
||||
// current buffer state without defining extra result types.
|
||||
func (workspace *Workspace) Query(query string) core.Result {
|
||||
if err := workspace.ensureReady("store.Workspace.Query"); err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue