tinyrelayfile
tinyrelay/internal/daemon/git_outbox_sync.go/

internal/daemon/git_outbox_sync.go raw
1package daemon
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "time"
8
9 "github.com/FelineStateMachine/tinyrelay/internal/event"
10 gitrelay "github.com/FelineStateMachine/tinyrelay/tinygit"
11 "github.com/FelineStateMachine/tinyrelay/internal/replication"
12 "github.com/FelineStateMachine/tinyrelay/internal/storage"
13)
14
15const gitOutboxOffsetPrefix = "grasp03.outbox-offset."
16
17type tenantGitOutboxTransport struct {
18 tenant *Tenant
19 repo gitrelay.Repository
20}
21
22func (t tenantGitOutboxTransport) Query(ctx context.Context, target string, filter event.Filter) ([]event.Event, error) {
23 requestCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
24 defer cancel()
25 transport, err := t.tenant.gitTargetTransport(requestCtx, t.repo, target)
26 if err != nil {
27 return nil, err
28 }
29 return transport.Query(requestCtx, target, filter)
30}
31
32// gitOutboxSync imports participant relay lists, then asks each discovered
33// outbox for the repository conversation. List events are ingested first so
34// the discovered routing remains durable after a restart.
35func (t *Tenant) gitOutboxSync(ctx context.Context, repo gitrelay.Repository) error {
36 if t == nil || t.store == nil {
37 return errors.New("GRASP-03: tenant store is required")
38 }
39 events, err := t.gitConversationEvents(ctx, repo)
40 if err != nil {
41 return err
42 }
43 authors := gitConversationAuthors(events, repo.Owner, repo.Maintainers)
44 offset := t.gitOutboxOffset(ctx, repo)
45 selectedAuthors := boundedGitAuthorsFrom(authors, offset%maxGitInt(len(authors), 1))
46 private := repo.Private || t.PrivateServiceEnabled()
47 options := gitOutboxOptions{Transport: tenantGitOutboxTransport{tenant: t, repo: repo}, Seeds: repo.Relays, Private: private, PrivatePeers: t.Policy().PrivatePeers, AuthorOffset: 0, SeedOffset: offset}
48 listEvents, err := t.gitLocalOutboxEvents(ctx, selectedAuthors)
49 if err != nil {
50 return err
51 }
52 discovered, discoverErr := discoverGitOutboxEvents(ctx, options, selectedAuthors)
53 listEvents = mergeGitOutboxEvents(listEvents, discovered)
54 var syncErrors []error
55 if err := t.ingestGitOutboxEvents(ctx, discovered); err != nil {
56 syncErrors = append(syncErrors, err)
57 }
58 if discoverErr != nil {
59 syncErrors = append(syncErrors, discoverErr)
60 }
61 if len(authors) > 0 {
62 if err := t.store.PutSetting(ctx, gitOutboxOffsetKey(repo), (offset+gitOutboxAuthorLimit)%len(authors)); err != nil {
63 syncErrors = append(syncErrors, fmt.Errorf("GRASP-03: save outbox offset: %w", err))
64 }
65 }
66 targets := gitOutboxRelays(listEvents, options.Private, options.PrivatePeers)
67 targets = rotateGitRelays(targets, offset)
68 for _, target := range targets {
69 targetCtx, cancel := context.WithTimeout(ctx, 8*time.Second)
70 if err := t.gitSyncPeer(targetCtx, repo, target); err != nil {
71 syncErrors = append(syncErrors, fmt.Errorf("GRASP-03: sync %s: %w", target, err))
72 }
73 cancel()
74 }
75 return errors.Join(syncErrors...)
76}
77
78func (t *Tenant) gitLocalOutboxEvents(ctx context.Context, authors []string) ([]event.Event, error) {
79 result := make([]event.Event, 0, len(authors)*3)
80 for _, author := range authors {
81 for _, kind := range []int{event.KIND_PROFILE, 10002, 10317} {
82 rows, err := t.store.Query(ctx, event.Filter{Authors: []string{author}, Kinds: []int{kind}, Tags: map[string][]string{}}, storage.QueryOptions{Now: time.Now().Unix(), Access: storage.Access{All: true}, Limit: 1})
83 if err != nil {
84 return nil, fmt.Errorf("GRASP-03: read cached outbox: %w", err)
85 }
86 if len(rows.Events) > 0 {
87 result = append(result, rows.Events[0])
88 }
89 }
90 }
91 return result, nil
92}
93
94func rotateGitRelays(relays []string, offset int) []string {
95 if len(relays) == 0 {
96 return nil
97 }
98 start := offset % len(relays)
99 return append(append([]string(nil), relays[start:]...), relays[:start]...)
100}
101
102func maxGitInt(a, b int) int {
103 if a > b {
104 return a
105 }
106 return b
107}
108
109func mergeGitOutboxEvents(existing, incoming []event.Event) []event.Event {
110 result := append([]event.Event(nil), existing...)
111 for _, item := range incoming {
112 found := false
113 for i := range result {
114 if result[i].PubKey != item.PubKey || result[i].Kind != item.Kind {
115 continue
116 }
117 found = true
118 if item.CreatedAt > result[i].CreatedAt || (item.CreatedAt == result[i].CreatedAt && item.ID < result[i].ID) {
119 result[i] = item
120 }
121 break
122 }
123 if !found {
124 result = append(result, item)
125 }
126 }
127 return result
128}
129
130func gitOutboxRelays(items []event.Event, private bool, peers []string) []string {
131 result := make([]string, 0, gitOutboxRelayLimit)
132 seen := make(map[string]struct{}, gitOutboxRelayLimit)
133 for _, item := range items {
134 for _, raw := range gitOutboxRelayTags(item) {
135 if !gitOutboxAllowedRelay(raw, private, peers) {
136 continue
137 }
138 canonical := normalizeRelayGit(raw)
139 if _, ok := seen[canonical]; ok {
140 continue
141 }
142 seen[canonical] = struct{}{}
143 result = append(result, canonical)
144 if len(result) == gitOutboxRelayLimit {
145 return result
146 }
147 }
148 }
149 return result
150}
151
152func (t *Tenant) ingestGitOutboxEvents(ctx context.Context, items []event.Event) error {
153 for _, item := range items {
154 if err := t.ingest(ctx, item, replication.OriginImport); err != nil && !errors.Is(err, storage.ErrDuplicate) && !errors.Is(err, storage.ErrReplaced) {
155 return fmt.Errorf("GRASP-03: import outbox list: %w", err)
156 }
157 }
158 return nil
159}
160
161func (t *Tenant) gitOutboxOffset(ctx context.Context, repo gitrelay.Repository) int {
162 var offset int
163 if err := t.store.GetSetting(ctx, gitOutboxOffsetKey(repo), &offset); err != nil || offset < 0 {
164 return 0
165 }
166 return offset
167}
168
169func gitOutboxOffsetKey(repo gitrelay.Repository) string {
170 return gitOutboxOffsetPrefix + repo.Owner + ":" + repo.Identifier
171}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs