scope

package
v0.12.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 9, 2026 License: MIT Imports: 3 Imported by: 0

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

View Source
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(next http.Handler, scopeOf func(*http.Request) Scope) http.Handler

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

func Parse(str string) Scope

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) IsZero

func (s Scope) IsZero() bool

IsZero reports whether every field is empty.

func (Scope) String

func (s Scope) String() 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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL