~bigbes/sr-ht-spec

ref: 865a21fa60c6230f79a17ef880767c87729c0435 sr-ht-spec/web/reader.go -rw-r--r-- 6.6 KiB
865a21fa — Eugene Blikh feat(web): digest tracks 'since you last looked' via digest_mark (spec-mfm) 24 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
package web

import (
	"context"
	"time"

	"sourcecraft.dev/bigbes/sr-ht-spec/core"
	"sourcecraft.dev/bigbes/sr-ht-spec/doc"
	"sourcecraft.dev/bigbes/sr-ht-spec/search"
	"sourcecraft.dev/bigbes/sr-ht-spec/service"
)

// Snapshot is one space at one *resolved* revision: the addressable document
// set plus every document's bytes.
//
// Rev is always a commit sha, never a branch name, even when the request
// carried no ?rev= at all. The read plane resolves first and renders second, so
// a merge landing mid-request cannot make one page describe two revisions —
// and the sha it resolved to is the value the page offers as its permalink.
//
// Bodies is keyed by tree path, the same key doc.Page.Path carries. It comes
// off the same tree walk as the archive, so a document listed here and a
// document in the archive are the same bytes at the same revision.
type Snapshot struct {
	Ref     core.SpaceRef
	Rev     string
	Archive *doc.Archive
	Bodies  map[string][]byte
}

// Reader is the service surface this package needs. *service.Service provides
// it through NewReader.
//
// It is an interface for the same reason compare.sr.ht's authz.Authorizer is:
// so the handlers can be tested against a document set rather than against a
// Postgres instance and a tree of bare repositories. It is named Reader for the
// read plane it began as; Phase 4's review page adds the proposal reads and the
// two approve/reject writes, because the review page is where this package first
// mutates anything and one seam is simpler than two.
type Reader interface {
	// ListSpaces returns every space on the instance. Single-user with no
	// visibility levels means there is nothing to filter — the list is the
	// whole corpus.
	ListSpaces(ctx context.Context) ([]core.SpaceRef, error)

	// Snapshot resolves rev (service.ApprovedRev for the approved head) and
	// returns the space at that revision.
	Snapshot(ctx context.Context, ref core.SpaceRef, rev string) (*Snapshot, error)

	// ReadDocument reads one document by tree path at a revision.
	ReadDocument(ctx context.Context, ref core.SpaceRef, rev, path string) (service.Document, error)

	// GetProposal resolves one proposal by id, in any state — the stable
	// proposal URL still resolves after merge or rejection.
	GetProposal(ctx context.Context, id int) (service.Proposal, error)

	// ListProposals returns a space's proposals in one state, newest first: the
	// inbox (open) and the digest (merged) are the same call with a different
	// state.
	ListProposals(ctx context.Context, ref core.SpaceRef, state core.ProposalState) ([]service.Proposal, error)

	// ProposalDiff returns each document a proposal changes, with the base and
	// proposed content the page diffs.
	ProposalDiff(ctx context.Context, p service.Proposal) ([]service.ProposalDoc, error)

	// Approve merges a proposal on the owner's approval — always human, never
	// policy. Reject resolves it to rejected. Both return the proposal as it now
	// stands.
	Approve(ctx context.Context, ref core.SpaceRef, id int) (service.Proposal, error)
	Reject(ctx context.Context, ref core.SpaceRef, id int) (service.Proposal, error)

	// Inbox is every open proposal on the instance, newest first — the review
	// queue. Digest is the recently policy-merged proposals, the firehose a human
	// sees after the fact.
	Inbox(ctx context.Context) ([]service.Proposal, error)
	Digest(ctx context.Context) ([]service.Proposal, error)

	// DigestMark reports when the owner last marked the digest seen, and whether
	// a mark exists yet; the inbox reads it to divide new auto-merges from seen
	// ones and itself never writes. MarkDigestSeen advances it — the one write
	// the review queue makes, behind an explicit POST so the GET stays pure.
	DigestMark(ctx context.Context) (time.Time, bool, error)
	MarkDigestSeen(ctx context.Context, seenAt time.Time) error
}

// Searcher is the keyword index. *search.Index satisfies it as declared.
type Searcher interface {
	Search(ctx context.Context, q search.Query) (search.Results, error)
}

// NewReader adapts a *service.Service to Reader.
func NewReader(svc *service.Service) Reader { return serviceReader{svc: svc} }

// serviceReader is the production Reader: everything goes through service/,
// which is the layer that is allowed to touch gitx and db.
type serviceReader struct{ svc *service.Service }

func (r serviceReader) ListSpaces(ctx context.Context) ([]core.SpaceRef, error) {
	spaces, err := r.svc.ListSpaces(ctx)
	if err != nil {
		return nil, err
	}
	refs := make([]core.SpaceRef, 0, len(spaces))
	for _, sp := range spaces {
		refs = append(refs, sp.Ref)
	}
	return refs, nil
}

func (r serviceReader) Snapshot(ctx context.Context, ref core.SpaceRef, rev string) (*Snapshot, error) {
	sp, err := r.svc.OpenSpace(ctx, ref)
	if err != nil {
		return nil, err
	}
	// One call, one tree walk, and the link graph filled in. This package used
	// to scan the git tree itself and then list the documents a second time for
	// their bodies — two walks per page view, and a reach past service/ into
	// gitx that the layering rule forbids.
	arc, bodies, err := r.svc.Archive(ctx, sp, rev)
	if err != nil {
		return nil, err
	}
	return &Snapshot{Ref: ref, Rev: arc.Rev, Archive: arc, Bodies: bodies}, nil
}

func (r serviceReader) ReadDocument(ctx context.Context, ref core.SpaceRef, rev, p string) (service.Document, error) {
	sp, err := r.svc.OpenSpace(ctx, ref)
	if err != nil {
		return service.Document{}, err
	}
	return r.svc.ReadDocument(ctx, sp, rev, p)
}

func (r serviceReader) GetProposal(ctx context.Context, id int) (service.Proposal, error) {
	return r.svc.GetProposal(ctx, id)
}

func (r serviceReader) ListProposals(ctx context.Context, ref core.SpaceRef, state core.ProposalState) ([]service.Proposal, error) {
	return r.svc.ListProposals(ctx, ref, state)
}

func (r serviceReader) ProposalDiff(ctx context.Context, p service.Proposal) ([]service.ProposalDoc, error) {
	return r.svc.ProposalDiff(ctx, p)
}

func (r serviceReader) Approve(ctx context.Context, ref core.SpaceRef, id int) (service.Proposal, error) {
	return r.svc.MergeHuman(ctx, ref, id)
}

func (r serviceReader) Reject(ctx context.Context, ref core.SpaceRef, id int) (service.Proposal, error) {
	return r.svc.Reject(ctx, ref, id)
}

func (r serviceReader) Inbox(ctx context.Context) ([]service.Proposal, error) {
	return r.svc.InboxProposals(ctx)
}

func (r serviceReader) Digest(ctx context.Context) ([]service.Proposal, error) {
	return r.svc.DigestProposals(ctx, 0)
}

func (r serviceReader) DigestMark(ctx context.Context) (time.Time, bool, error) {
	return r.svc.DigestMark(ctx)
}

func (r serviceReader) MarkDigestSeen(ctx context.Context, seenAt time.Time) error {
	return r.svc.MarkDigestSeen(ctx, seenAt)
}