Documentation
¶
Overview ¶
Package scope is the Go side of the devtools' Scope (plan §13.3): the unit every discovery rung, deep link and API call carries — one conversation's public id and, optionally, the session, flow and run inside it — and the net/http middleware that hands it to the page.
A Scope has one string form, shared byte for byte with the web client's lib/scope.ts (the golden studio/testdata/scope.golden.json pins both):
pub_…;session=s_…;flow=f_…;run=r_…
the public id first and bare, the rest optional, in that fixed order, empty fields omitted, each value percent-encoded with encodeURIComponent's rules. Parse reads it back and never fails: a newer writer's field is not this reader's to reject.
The response header is the development rung: thread has no HTTP layer, so the app's own handler sets it, in one line —
mux.Handle("POST /chat", scope.Header(chat, func(r *http.Request) scope.Scope {
return scope.Scope{PublicID: publicIDOf(r)}
}))
and a handler that learns the run id only once its turn starts calls scope.Set(w, …) before it writes. The header never carries a token; a Scope has nowhere to put one.
The package lives beside the root rather than in it: the root package is a generated facade over core, which never imports net/http (ADR 0016, ADR 0027).
Index ¶
Examples ¶
Constants ¶
const HeaderName = "Weft-Scope"
HeaderName is the response header the app's handler sets and the panel reads.
Variables ¶
This section is empty.
Functions ¶
func Header ¶
Header sets the Weft-Scope response header to scopeOf(r) on every response next writes, unless that scope is zero, and exposes the header to cross-origin pages (Access-Control-Expose-Headers gains Weft-Scope; the app's CORS policy must still allow the page's origin). The header is set before next runs, so a streaming handler's first flush carries it; next may replace it with Set. A CORS layer inside Header that sets its own expose list replaces the entry, so the header is sent but a cross-origin page cannot read it: put scope.Header inside your CORS middleware, or list Weft-Scope in its exposed headers. The devtools panel reads a cross-origin response's header only when the page and the response are both on loopback (a dev server on :5173 calling the app on :8080); a cross-origin production app names its scope with data-scope or the DOM marker instead.
Example ¶
Header on the app's own chat endpoint, in one line: every response carries the conversation's scope, and the handler narrows it to the run once the turn has started. Cross-origin pages read the header through Access-Control-Expose-Headers, which Header sets; the app's CORS policy still has to allow the page's origin.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/weftgo/weft/scope"
)
func main() {
chat := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
runID := "r_01" // turn.RunID() once the turn is sent
scope.Set(w, scope.Scope{PublicID: r.URL.Query().Get("c"), RunID: runID})
_, _ = fmt.Fprintln(w, "hello")
})
h := scope.Header(chat, func(r *http.Request) scope.Scope {
return scope.Scope{PublicID: r.URL.Query().Get("c")}
})
w := httptest.NewRecorder()
h.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/chat?c=pub_demo", nil))
fmt.Println(w.Header().Get("Weft-Scope"))
fmt.Println(w.Header().Get("Access-Control-Expose-Headers"))
}
Output: pub_demo;run=r_01 Weft-Scope
func Set ¶
func Set(w http.ResponseWriter, s Scope)
Set sets the Weft-Scope header on w to s, replacing any earlier value, and exposes it to cross-origin pages (Weft-Scope is appended to Access-Control-Expose-Headers unless already listed; a "*" there does not cover it for a credentialed request). A zero s removes the Weft-Scope header but leaves the expose entry an earlier Set added. Like any header it must be set before the handler's first Write or WriteHeader — the dynamic case: a handler that knows the run id once its turn starts; a Set after that changes nothing sent.
Types ¶
type Scope ¶
type Scope struct {
PublicID string `json:"publicId"`
SessionID string `json:"session,omitempty"`
FlowID string `json:"flow,omitempty"`
RunID string `json:"run,omitempty"`
}
Scope names what the devtools follow. PublicID is the join key the app hands out ("" is the dev list: no conversation); the other fields narrow it. The JSON names are the web client's.
func Parse ¶
Parse reads a scope's string form back. The first ";"-segment is the public id; each later one is key=value. A key it does not know, a segment without "=", an empty value and a repeat of a key already read are ignored; a value that does not percent-decode is kept raw. Parse never fails.
Example ¶
package main
import (
"fmt"
"github.com/weftgo/weft/scope"
)
func main() {
s := scope.Parse("pub_demo;flow=f_3;tenant=acme;run=r_9")
fmt.Printf("%q %q %q %q\n", s.PublicID, s.SessionID, s.FlowID, s.RunID)
fmt.Println(s)
}
Output: "pub_demo" "" "f_3" "r_9" pub_demo;flow=f_3;run=r_9
func (Scope) String ¶
String is the scope's one string form: the public id first and bare, then session=, flow=, run= for each field set, joined by ";", every value percent-encoded. The zero Scope is "".
Example ¶
package main
import (
"fmt"
"github.com/weftgo/weft/scope"
)
func main() {
fmt.Println(scope.Scope{PublicID: "pub_demo", SessionID: "s_1", RunID: "r_2"})
fmt.Println(scope.Scope{PublicID: "a;b"})
}
Output: pub_demo;session=s_1;run=r_2 a%3Bb