package stateobjects import ( "errors" "fmt" "strings" "time" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/buscalls" "git.novox.be/novox/mesh-sdk/go/views" ) // SentMaxAge is how long a store of sent objects keeps one: transport, not kept data (ADR 0595 §4), so the store ages // its objects out by itself and the drop tool is for the case where it does not. const SentMaxAge = 7 * 24 * time.Hour // State is one state of a module with its objects: the state's name (its local name in the manifest), its values and the // fill of its object store against its cap. type State struct { Name string Values Values Fill func() (buscalls.Fill, error) } // Sent is a module's store of sent objects, read and dropped by the module's own tools. type Sent struct { Objects Objects Fill func() (buscalls.Fill, error) // MaxAge is the store's age (SentMaxAge when zero), which the listing compares each object's age with. MaxAge time.Duration } // Tools are the four tools of a module that keeps large values or sends large bodies (design 25 § 12.2, ADR 0666 §25), // named for the module the caller gives: `_state_objects` and `_state_objects_drop` over the states, and // `_sent_objects` and `_sent_objects_drop` over the store of sent objects. The two reading tools read // everything the store holds; the two dropping tools remove only what is no longer live, each only with the person who // asks (`by`) and why, recorded as a hand act before anything is removed, and refuse one that cannot be recorded. func Tools(module string, states []State, sent *Sent, acts views.HandActs, now func() time.Time) []stdio.Tool { var tools []stdio.Tool if len(states) > 0 { tools = append(tools, stateObjectsTool(module, states), stateDropTool(module, states, acts)) } if sent != nil { tools = append(tools, sentObjectsTool(module, *sent, now), sentDropTool(module, *sent, acts, now)) } return tools } func pick(states []State, args map[string]any) (State, error) { name, _ := args["state"].(string) name = strings.TrimSpace(name) if name == "" && len(states) == 1 { return states[0], nil } for _, s := range states { if s.Name == name { return s, nil } } var names []string for _, s := range states { names = append(names, s.Name) } return State{}, fmt.Errorf("state names one of %s, not %q", strings.Join(names, ", "), name) } var stateArg = map[string]any{"type": "string", "description": "the state whose object store is read: its local name in the manifest"} func stateObjectsTool(module string, states []State) stdio.Tool { return stdio.Tool{ Name: module + "_state_objects", Description: "The objects that hold the large values of " + module + "'s states: each object, its size, the key whose value names it " + "or key-gone when the key was deleted (such an object stays until " + module + "_state_objects_drop removes it on a person's word), " + "and how full the object store is against its cap.", Input: map[string]any{"type": "object", "properties": map[string]any{"state": stateArg}}, Run: func(args map[string]any) (any, error) { s, err := pick(states, args) if err != nil { return nil, err } kept, err := s.Values.Kept() if err != nil { return nil, err } out := map[string]any{"state": s.Name, "objects": kept} gone := 0 for _, k := range kept { if k.KeyGone { gone++ } } out["key-gone"] = gone if s.Fill != nil { if f, err := s.Fill(); err == nil { out["fill"] = f } else { out["fill-error"] = err.Error() } } return out, nil }, } } var byWhy = map[string]any{ "by": map[string]any{"type": "string", "description": "who asks, as who@node"}, "why": map[string]any{"type": "string", "description": "why, in plain words"}, } // asked is by and why of a dropping call: both are needed, or nothing is done. func asked(args map[string]any) (by, why string, err error) { by, _ = args["by"].(string) why, _ = args["why"].(string) by, why = strings.TrimSpace(by), strings.TrimSpace(why) if by == "" || why == "" { return "", "", errors.New("a drop names who asks (by) and why: nothing was dropped") } return by, why, nil } func record(acts views.HandActs, tool, what, by, why string) error { if acts == nil { return errors.New("no way to record a hand act is given, so nothing was dropped") } if err := acts.RecordHandAct(views.HandAct{What: what + " (asked by " + by + ")", Why: why, Cause: "objects-dropped", Verb: tool}); err != nil { return fmt.Errorf("the hand act was not recorded, so nothing was dropped: %w", err) } return nil } func stateDropTool(module string, states []State, acts views.HandActs) stdio.Tool { name := module + "_state_objects_drop" props := map[string]any{"state": stateArg} for k, v := range byWhy { props[k] = v } return stdio.Tool{ Name: name, Description: "Drop the objects of one of " + module + "'s states whose key is gone, on a person's word (ADR 0595 §4, ADR 0666 §16). " + "An object a key names is never dropped. Recorded as a hand act first; needs by and why.", Input: map[string]any{"type": "object", "required": []string{"by", "why"}, "properties": props}, Run: func(args map[string]any) (any, error) { by, why, err := asked(args) if err != nil { return nil, err } s, err := pick(states, args) if err != nil { return nil, err } if err := record(acts, name, "dropped the objects of "+module+"'s state "+s.Name+" whose key is gone", by, why); err != nil { return nil, err } dropped, err := s.Values.DropKeyGone() return map[string]any{"state": s.Name, "dropped": dropped}, err }, } } // SentObject is one object of a store of sent objects as the listing shows it. type SentObject struct { Name string `json:"name"` // Label is the verb or event the body was sent for. Label string `json:"label"` Size int64 `json:"size"` Sent time.Time `json:"sent,omitzero"` Age string `json:"age"` PastAge bool `json:"past-age,omitempty"` } func (s Sent) maxAge() time.Duration { if s.MaxAge > 0 { return s.MaxAge } return SentMaxAge } func (s Sent) list(now time.Time) ([]SentObject, error) { objects, err := s.Objects.List() if err != nil { return nil, err } out := make([]SentObject, 0, len(objects)) for _, o := range objects { age := now.Sub(o.Modified) out = append(out, SentObject{Name: o.Name, Label: keyOf(o.Name), Size: o.Size, Sent: o.Modified, Age: age.Round(time.Second).String(), PastAge: age > s.maxAge()}) } return out, nil } func sentObjectsTool(module string, s Sent, now func() time.Time) stdio.Tool { return stdio.Tool{ Name: module + "_sent_objects", Description: "The objects " + module + " sent in place of a reply or an event over the message limit: each object, its size, when it " + "was sent, the verb or event it was sent for, its age against the store's age (" + s.maxAge().String() + ", transport and not kept data), " + "and how full the store is against its cap.", Run: func(map[string]any) (any, error) { objects, err := s.list(now()) if err != nil { return nil, err } out := map[string]any{"objects": objects, "max-age": s.maxAge().String()} if s.Fill != nil { if f, err := s.Fill(); err == nil { out["fill"] = f } else { out["fill-error"] = err.Error() } } return out, nil }, } } func sentDropTool(module string, s Sent, acts views.HandActs, now func() time.Time) stdio.Tool { name := module + "_sent_objects_drop" props := map[string]any{"older-than": map[string]any{"type": "string", "description": "a duration such as 24h: only objects sent longer ago are dropped"}} for k, v := range byWhy { props[k] = v } return stdio.Tool{ Name: name, Description: "Drop the sent objects of " + module + " older than a given age, on a person's word. Younger objects are live and stay. " + "Recorded as a hand act first; needs by, why and older-than.", Input: map[string]any{"type": "object", "required": []string{"by", "why", "older-than"}, "properties": props}, Run: func(args map[string]any) (any, error) { by, why, err := asked(args) if err != nil { return nil, err } text, _ := args["older-than"].(string) age, err := time.ParseDuration(strings.TrimSpace(text)) if err != nil || age <= 0 { return nil, fmt.Errorf("older-than %q is not a duration above zero such as 24h: nothing was dropped", text) } if err := record(acts, name, "dropped the sent objects of "+module+" older than "+age.String(), by, why); err != nil { return nil, err } objects, err := s.Objects.List() if err != nil { return nil, err } var dropped []string for _, o := range objects { if now().Sub(o.Modified) <= age { continue } if err := s.Objects.Delete(o.Name); err != nil { return map[string]any{"dropped": dropped}, fmt.Errorf("the object %s: %w", o.Name, err) } dropped = append(dropped, o.Name) } return map[string]any{"dropped": dropped}, nil }, } }