package store import ( "bufio" "encoding/json" "errors" "fmt" "orchestra/internal/domain" "os" "path/filepath" "sync" "time" ) type Store struct { mu sync.Mutex path string cas string events []domain.Event tasks map[string]domain.Task external map[string]string snapshot string seq uint64 } func Open(dir string) (*Store, error) { if err := os.MkdirAll(dir, 0755); err != nil { return nil, err } s := &Store{path: filepath.Join(dir, "events.jsonl"), cas: filepath.Join(dir, "cas"), snapshot: filepath.Join(dir, "snapshot.json"), tasks: map[string]domain.Task{}, external: map[string]string{}} if err := os.MkdirAll(s.cas, 0755); err != nil { return nil, err } var snapshotSeq uint64 if b, readErr := os.ReadFile(s.snapshot); readErr == nil { var snap struct { Seq uint64 `json:"seq"` Tasks []domain.Task `json:"tasks"` } if json.Unmarshal(b, &snap) != nil { return nil, fmt.Errorf("invalid snapshot") } for _, t := range snap.Tasks { s.tasks[t.ID] = t s.external[t.Source+"\x00"+t.ExternalID] = t.ID } snapshotSeq = snap.Seq } else if !errors.Is(readErr, os.ErrNotExist) { return nil, readErr } f, err := os.Open(s.path) if os.IsNotExist(err) { return s, nil } if err != nil { return nil, err } defer f.Close() sc := bufio.NewScanner(f) var expected uint64 = snapshotSeq + 1 for sc.Scan() { var e domain.Event if err := json.Unmarshal(sc.Bytes(), &e); err == nil { if err := domain.ValidateEvent(e); err != nil { return nil, err } if e.Seq < expected { continue } if e.Seq != expected { return nil, fmt.Errorf("event sequence gap: got %d, want %d", e.Seq, expected) } if t, ok := s.tasks[e.TaskID]; ok && e.Version != t.Version+1 { return nil, domain.ErrConflict } if _, ok := s.tasks[e.TaskID]; !ok && e.Type != "TaskCreated" { return nil, domain.ErrNotFound } s.events = append(s.events, e) s.seq = e.Seq if err := s.apply(e); err != nil { return nil, err } expected++ } else { return nil, err } } return s, sc.Err() } func (s *Store) apply(e domain.Event) error { var p map[string]any if err := json.Unmarshal(e.Payload, &p); err != nil { return err } t := s.tasks[e.TaskID] if e.Type == "QuotaReported" || e.Type == "StandupAdvisory" { return nil } switch e.Type { case "TaskCreated": if err := domain.ValidateCreated(p); err != nil { return err } t = domain.Task{ID: e.TaskID, Source: p["source"].(string), ExternalID: p["external_id"].(string), Project: p["project"].(string), State: domain.StateQueued} if v, ok := p["parent"].(string); ok { t.Parent = v } if v, ok := p["inherent_priority"].(float64); ok { t.InherentPriority = int(v) } if v, ok := p["due"].(string); ok { if d, err := time.Parse(time.RFC3339, v); err == nil { t.Due = &d } } if v, ok := p["estimate"].(map[string]any); ok { t.Estimate = &domain.Estimate{} t.Estimate.Value, _ = v["value"].(float64) t.Estimate.Who, _ = v["who"].(string) t.Estimate.Confidence, _ = v["confidence"].(float64) } if v, ok := p["capability"].([]any); ok { for _, x := range v { if z, ok := x.(string); ok { t.Capability = append(t.Capability, z) } } } if v, ok := p["title"].(string); ok { t.Title = v } s.external[t.Source+"\x00"+t.ExternalID] = t.ID case "TaskLeased": t.State = domain.StateLeased t.Lease = &domain.Lease{HarnessID: p["harness_id"].(string), Until: time.Unix(0, int64(p["until_ns"].(float64)))} case "TaskReleased": t.State = domain.StateQueued t.Lease = nil case "TaskCompleted": t.State = domain.StateCompleted t.Lease = nil case "TaskFailed": t.State = domain.StateFailed t.Lease = nil case "TaskBlocked": t.State = domain.StateBlocked t.Lease = nil case "TaskAmended": for k, v := range p { if k == "title" { t.Title, v = v.(string) } } } t.Version = e.Version s.tasks[e.TaskID] = t return nil } func (s *Store) Append(e domain.Event) error { s.mu.Lock() defer s.mu.Unlock() if err := domain.ValidateEvent(e); err != nil { return err } if e.At.IsZero() { e.At = time.Now().UTC() } if e.Seq == 0 { e.Seq = s.seq + 1 } if e.SchemaVersion == 0 { e.SchemaVersion = domain.CurrentEventSchema } if e.Type == "TaskCreated" { var p map[string]any if err := json.Unmarshal(e.Payload, &p); err != nil { return err } if id := s.external[p["source"].(string)+"\x00"+p["external_id"].(string)]; id != "" { return nil } } t, taskExists := s.tasks[e.TaskID] if taskExists && e.Version != t.Version+1 { return domain.ErrConflict } if e.Type == "TaskLeased" { var p struct { ExpectedVersion *int `json:"expected_version"` } if err := json.Unmarshal(e.Payload, &p); err != nil { return err } if p.ExpectedVersion != nil && (t.Version != *p.ExpectedVersion) { return domain.ErrConflict } } global := e.Type == "QuotaReported" || e.Type == "StandupAdvisory" if !taskExists && e.Type != "TaskCreated" && !global { return domain.ErrNotFound } if e.Type != "TaskCreated" && (e.Type == "TaskCompleted" || e.Type == "TaskBlocked" || e.Type == "TaskReleased") { var p map[string]any _ = json.Unmarshal(e.Payload, &p) for _, k := range []string{"handoff_ref", "report_ref"} { if ref, ok := p[k].(string); ok { if _, err := s.Artifact(ref); err != nil { return fmt.Errorf("%w: missing artifact %s", domain.ErrInvalid, ref) } } } } if err := s.apply(e); err != nil { return err } f, err := os.OpenFile(s.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) if err != nil { return err } defer f.Close() b, _ := json.Marshal(e) if _, err = f.Write(append(b, '\n')); err != nil { return err } if err = f.Sync(); err != nil { return err } s.events = append(s.events, e) s.seq = e.Seq if err := s.writeSnapshot(); err != nil { return err } return nil } func (s *Store) writeSnapshot() error { tasks := make([]domain.Task, 0, len(s.tasks)) for _, t := range s.tasks { tasks = append(tasks, t) } b, err := json.Marshal(struct { Seq uint64 `json:"seq"` Tasks []domain.Task `json:"tasks"` }{s.seq, tasks}) if err != nil { return err } tmp := s.snapshot + ".tmp" if err = os.WriteFile(tmp, b, 0644); err != nil { return err } return os.Rename(tmp, s.snapshot) } func (s *Store) Tasks() []domain.Task { s.mu.Lock() defer s.mu.Unlock() out := make([]domain.Task, 0, len(s.tasks)) for _, t := range s.tasks { out = append(out, t) } return out } func (s *Store) Events(since uint64) []domain.Event { s.mu.Lock() defer s.mu.Unlock() var out []domain.Event for _, e := range s.events { if e.Seq > since { out = append(out, e) } } return out } func (s *Store) PutArtifact(b []byte) (string, error) { h := domain.Hash(b) p := filepath.Join(s.cas, h) if _, err := os.Stat(p); errors.Is(err, os.ErrNotExist) { if err = os.WriteFile(p, b, 0644); err != nil { return "", err } } return h, nil } // Artifact returns a CAS artifact after verifying its content address. func (s *Store) Artifact(ref string) ([]byte, error) { if len(ref) != 64 { return nil, fmt.Errorf("%w: invalid artifact reference", domain.ErrInvalid) } b, err := os.ReadFile(filepath.Join(s.cas, ref)) if err != nil { return nil, err } if domain.Hash(b) != ref { return nil, fmt.Errorf("%w: corrupt artifact", domain.ErrInvalid) } return b, nil } func (s *Store) Task(id string) (domain.Task, bool) { s.mu.Lock() defer s.mu.Unlock() t, ok := s.tasks[id] return t, ok } func (s *Store) Lease(id, harness string, ttl time.Duration) (domain.Event, error) { t, ok := s.Task(id) if !ok { return domain.Event{}, domain.ErrNotFound } if t.State != domain.StateQueued { return domain.Event{}, domain.ErrConflict } p, _ := json.Marshal(map[string]any{"harness_id": harness, "until_ns": time.Now().Add(ttl).UnixNano(), "expected_version": t.Version}) e := domain.Event{ID: id, Type: "TaskLeased", TaskID: id, Version: t.Version + 1, Payload: p} return e, s.Append(e) } func (s *Store) ExpireLeases(now time.Time) ([]domain.Event, error) { var out []domain.Event for _, t := range s.Tasks() { if t.State == domain.StateLeased && t.Lease != nil && !t.Lease.Until.After(now) { p, _ := json.Marshal(map[string]any{"reason": "lease_expired", "harness_id": t.Lease.HarnessID}) e := domain.Event{ID: t.ID, Type: "TaskReleased", TaskID: t.ID, Version: t.Version + 1, Payload: p} if err := s.Append(e); err != nil { return out, err } out = append(out, e) } } return out, nil }