Reading Your Own Writes over HTTP
To let a caller read its own writes over HTTP, call the QueryRevisioned function instead of Query. Additionally, hand over a view that implements Revisioned, whose projection is tracked (see Tracking Revisions), and how long to wait at most:
httpapi.QueryRevisioned(
api,
mux,
"GET /api/books",
catalog,
toListBooks,
listBooks(catalog),
httpapi.DefaultWait,
)
After sending a command, the caller takes the highest ID from eventIds and sends it in the Wait-For-Revision header of the query:
curl http://localhost:8080/api/books \
-H "Wait-For-Revision: 1"
The route waits until the view has reached this revision, but at most for the given duration, which is five seconds for httpapi.DefaultWait. Then it answers with what the view holds, even if the time has run out. If the header does not contain a revision, the request is answered with 400 Bad Request.
Once the view has seen at least one event, the response contains the revision it shows in the X-Revision header, as well as an ETag header and Cache-Control: no-cache. If the caller sends the ETag in the If-None-Match header and the view has not changed since, the request is answered with 304 Not Modified.
Note that the constants httpapi.HeaderWaitFor and httpapi.HeaderRevision contain the names of the two headers.
Depending on More Than the Read Model#
If an answer depends on more than the view, for example on the current date, call the QueryVarying function instead, and additionally hand over a function of the type httpapi.Volatile. It receives the request and returns a value that changes whenever the answer would, and that becomes part of the ETag:
type ListOverdueBooks struct {
Today string
}
func listOverdueBooks(catalog architecturekit.View[BookItem]) func(context.Context, ListOverdueBooks) ([]BookItem, error) {
return func(ctx context.Context, q ListOverdueBooks) ([]BookItem, error) {
items, err := catalog.All(ctx)
if err != nil {
return nil, err
}
return slices.Collect(query.Where(items, func(item BookItem) bool {
return item.IsBorrowed && item.BorrowedUntil < q.Today
})), nil
}
}
func today(*http.Request) string {
return time.Now().Format(time.DateOnly)
}
httpapi.QueryVarying(
api,
mux,
"GET /api/overdue-books",
catalog,
func(r *http.Request, user User) (ListOverdueBooks, error) {
return ListOverdueBooks{Today: today(r)}, nil
},
listOverdueBooks(catalog),
httpapi.DefaultWait,
today,
)
Building Your Own Revisioned Handler#
To build a handler of your own that works like QueryRevisioned, use these three functions:
Awaitwaits for the revision the request asks for. Running out of time is not an error. It returns an error if the header does not contain a revision, or if waiting fails for another reason.ServeUnchangedanswers with304 Not Modifiedif the caller already holds the given revision, and reports whether it did.RespondResultAtanswers likeRespondResult, and adds the headers for the given revision.
The last argument of ServeUnchanged and RespondResultAt is a Volatile function, or nil:
mux.HandleFunc("GET /api/books", func(w http.ResponseWriter, r *http.Request) {
if _, err := httpapi.UserOf(r, api); err != nil {
httpapi.RespondResult(w, struct{}{}, err)
return
}
if err := httpapi.Await(r.Context(), r, catalog, httpapi.DefaultWait); err != nil {
httpapi.RespondResult(w, struct{}{}, err)
return
}
revision := catalog.Revision()
if httpapi.ServeUnchanged(w, r, revision, nil) {
return
}
books, err := httpapi.Ask(r, api, toListBooks, listBooks(catalog))
httpapi.RespondResultAt(w, r, revision, books, err, nil)
})