assets API

assets

package

API reference for the assets package.

S
struct

imageEntry

imageEntry holds the result of an image load.

assets/assets.go:62-67
type imageEntry struct

Fields

Name Type Description
once sync.Once
img js.Value
err error
ready chan struct{}
F
function

SetNativeClient

SetNativeClient has no effect in browser builds.

Parameters

assets/assets.go:72-72
func SetNativeClient(_ *stdhttp.Client)

{}
F
function

LoadImage

LoadImage asynchronously loads an image from url.
While loading it returns http.ErrPending.
Results are cached by URL.

Parameters

url
string

Returns

error
assets/assets.go:77-98
func LoadImage(url string) (js.Value, error)

{
	ceIface, _ := imageCache.LoadOrStore(url, &imageEntry{ready: make(chan struct{})})
	ce := ceIface.(*imageEntry)

	ce.once.Do(func() {
		go loadImageFn(url, func(v js.Value, err error) {
			ce.img = v
			ce.err = err
			close(ce.ready)
		})
	})

	select {
	case <-ce.ready:
		if ce.err != nil {
			return js.Value{}, ce.err
		}
		return ce.img, nil
	default:
		return js.Value{}, http.ErrPending
	}
}
S
struct

modelEntry

modelEntry holds the result of a binary load.

assets/assets.go:101-106
type modelEntry struct

Fields

Name Type Description
once sync.Once
data []byte
err error
ready chan struct{}
F
function

LoadModel

LoadModel fetches binary data from url. It caches results and
returns http.ErrPending while the request is in flight.

Parameters

url
string

Returns

[]byte
error
assets/assets.go:112-133
func LoadModel(url string) ([]byte, error)

{
	ceIface, _ := modelCache.LoadOrStore(url, &modelEntry{ready: make(chan struct{})})
	ce := ceIface.(*modelEntry)

	ce.once.Do(func() {
		go loadBinaryFn(url, func(b []byte, err error) {
			ce.data = b
			ce.err = err
			close(ce.ready)
		})
	})

	select {
	case <-ce.ready:
		if ce.err != nil {
			return nil, ce.err
		}
		return ce.data, nil
	default:
		return nil, http.ErrPending
	}
}
F
function

LoadJSON

LoadJSON delegates to http.FetchJSON and shares its caching behavior.

Parameters

url
string
v
any

Returns

error
assets/assets.go:136-136
func LoadJSON(url string, v any) error

{ return http.FetchJSON(url, v) }
F
function

ClearCache

ClearCache removes cached assets for url.

Parameters

url
string
assets/assets.go:139-142
func ClearCache(url string)

{
	imageCache.Delete(url)
	modelCache.Delete(url)
}
F
function

SetNativeClient

SetNativeClient replaces the native asset client. Custom clients may reach
private networks and must only receive trusted URLs. Passing nil restores
the default client, which rejects private network addresses.

Parameters

client
assets/assets_stub.go:23-30
func SetNativeClient(client *stdhttp.Client)

{
	if client == nil {
		client = safehttp.NewClient()
	}
	httpClientMu.Lock()
	httpClient = client
	httpClientMu.Unlock()
}
F
function

currentNativeClient

Returns

assets/assets_stub.go:32-37
func currentNativeClient() *stdhttp.Client

{
	httpClientMu.RLock()
	client := httpClient
	httpClientMu.RUnlock()
	return client
}
F
function

fetch

Parameters

url
string

Returns

assets/assets_stub.go:39-45
func fetch(url string) (*stdhttp.Response, error)

{
	req, err := safehttp.NewRequest(context.Background(), stdhttp.MethodGet, url)
	if err != nil {
		return nil, err
	}
	return currentNativeClient().Do(req)
}
S
struct

Image

Image is a placeholder for non-WASM builds.

assets/assets_stub.go:48-51
type Image struct

Fields

Name Type Description
URL string
Data []byte
S
struct

imageEntry

assets/assets_stub.go:101-106
type imageEntry struct

Fields

Name Type Description
once sync.Once
img Image
err error
ready chan struct{}
F
function

LoadImage

LoadImage starts or reads a cached image request.

Parameters

url
string

Returns

error
assets/assets_stub.go:111-132
func LoadImage(url string) (Image, error)

{
	ceIface, _ := imageCache.LoadOrStore(url, &imageEntry{ready: make(chan struct{})})
	ce := ceIface.(*imageEntry)

	ce.once.Do(func() {
		go loadImageFn(url, func(v Image, err error) {
			ce.img = v
			ce.err = err
			close(ce.ready)
		})
	})

	select {
	case <-ce.ready:
		if ce.err != nil {
			return Image{}, ce.err
		}
		return ce.img, nil
	default:
		return Image{}, http.ErrPending
	}
}
S
struct

modelEntry

assets/assets_stub.go:134-139
type modelEntry struct

Fields

Name Type Description
once sync.Once
data []byte
err error
ready chan struct{}
F
function

LoadModel

LoadModel starts or reads a cached binary model request.

Parameters

url
string

Returns

[]byte
error
assets/assets_stub.go:144-165
func LoadModel(url string) ([]byte, error)

{
	ceIface, _ := modelCache.LoadOrStore(url, &modelEntry{ready: make(chan struct{})})
	ce := ceIface.(*modelEntry)

	ce.once.Do(func() {
		go loadBinaryFn(url, func(b []byte, err error) {
			ce.data = b
			ce.err = err
			close(ce.ready)
		})
	})

	select {
	case <-ce.ready:
		if ce.err != nil {
			return nil, ce.err
		}
		return ce.data, nil
	default:
		return nil, http.ErrPending
	}
}
F
function

LoadJSON

LoadJSON retrieves and decodes JSON from url.

Parameters

url
string
v
any

Returns

error
assets/assets_stub.go:168-168
func LoadJSON(url string, v any) error

{ return http.FetchJSON(url, v) }
F
function

ClearCache

ClearCache removes cached assets for url.

Parameters

url
string
assets/assets_stub.go:171-174
func ClearCache(url string)

{
	imageCache.Delete(url)
	modelCache.Delete(url)
}
F
function

waitImage

Parameters

fn
func() (Image, error)

Returns

assets/assets_test.go:15-31
func waitImage(t *testing.T, fn func() (Image, error)) Image

{
	t.Helper()
	deadline := time.Now().Add(2 * time.Second)
	for {
		img, err := fn()
		if err == nil {
			return img
		}
		if err != v1http.ErrPending {
			t.Fatalf("unexpected error: %v", err)
		}
		if time.Now().After(deadline) {
			t.Fatalf("timed out waiting for image")
		}
		time.Sleep(5 * time.Millisecond)
	}
}
F
function

waitBytes

Parameters

fn
func() ([]byte, error)

Returns

[]byte
assets/assets_test.go:33-49
func waitBytes(t *testing.T, fn func() ([]byte, error)) []byte

{
	t.Helper()
	deadline := time.Now().Add(2 * time.Second)
	for {
		b, err := fn()
		if err == nil {
			return b
		}
		if err != v1http.ErrPending {
			t.Fatalf("unexpected error: %v", err)
		}
		if time.Now().After(deadline) {
			t.Fatalf("timed out waiting for bytes")
		}
		time.Sleep(5 * time.Millisecond)
	}
}
F
function

TestFetchRejectsNonHTTPURLs

Parameters

assets/assets_test.go:51-66
func TestFetchRejectsNonHTTPURLs(t *testing.T)

{
	for _, rawURL := range []string{"file:///tmp/secret", "/relative", "http://127.0.0.1/secret"} {
		resp, err := fetch(rawURL)
		if resp != nil {
			if closeErr := resp.Body.Close(); closeErr != nil {
				t.Fatalf("close response for %q: %v", rawURL, closeErr)
			}
		}
		if err == nil {
			t.Fatalf("expected %q to be rejected", rawURL)
		}
		if strings.HasPrefix(rawURL, "http://127.") && !strings.Contains(err.Error(), "not public") {
			t.Fatalf("unexpected private URL rejection: %v", err)
		}
	}
}
F
function

TestLoadModel_CacheAndPending

Parameters

assets/assets_test.go:68-104
func TestLoadModel_CacheAndPending(t *testing.T)

{
	var hits int
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		hits++
		time.Sleep(20 * time.Millisecond)
		w.WriteHeader(200)
		_, _ = w.Write([]byte{1, 2, 3})
	}))
	defer srv.Close()
	oldClient := currentNativeClient()
	SetNativeClient(srv.Client())
	t.Cleanup(func() { SetNativeClient(oldClient) })

	ClearCache(srv.URL)
	t.Cleanup(func() { ClearCache(srv.URL) })

	if _, err := LoadModel(srv.URL); err != v1http.ErrPending {
		t.Fatalf("expected ErrPending, got %v", err)
	}

	got := waitBytes(t, func() ([]byte, error) { return LoadModel(srv.URL) })
	if len(got) != 3 || got[0] != 1 || got[2] != 3 {
		t.Fatalf("unexpected bytes: %v", got)
	}

	got2, err := LoadModel(srv.URL)
	if err != nil {
		t.Fatalf("expected cached success, got %v", err)
	}
	if len(got2) != 3 || got2[1] != 2 {
		t.Fatalf("unexpected cached bytes: %v", got2)
	}

	if hits != 1 {
		t.Fatalf("expected 1 server hit, got %d", hits)
	}
}
F
function

TestLoadImage_UsesCache

Parameters

assets/assets_test.go:106-142
func TestLoadImage_UsesCache(t *testing.T)

{
	var hits int
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		hits++
		time.Sleep(15 * time.Millisecond)
		w.WriteHeader(200)
		_, _ = w.Write([]byte("PNGDATA"))
	}))
	defer srv.Close()
	oldClient := currentNativeClient()
	SetNativeClient(srv.Client())
	t.Cleanup(func() { SetNativeClient(oldClient) })

	ClearCache(srv.URL)
	t.Cleanup(func() { ClearCache(srv.URL) })

	if _, err := LoadImage(srv.URL); err != v1http.ErrPending {
		t.Fatalf("expected ErrPending, got %v", err)
	}

	img := waitImage(t, func() (Image, error) { return LoadImage(srv.URL) })
	if img.URL != srv.URL || string(img.Data) != "PNGDATA" {
		t.Fatalf("unexpected image: %+v", img)
	}

	img2, err := LoadImage(srv.URL)
	if err != nil {
		t.Fatalf("expected cached image, got %v", err)
	}
	if string(img2.Data) != "PNGDATA" {
		t.Fatalf("unexpected cached data: %q", string(img2.Data))
	}

	if hits != 1 {
		t.Fatalf("expected 1 hit, got %d", hits)
	}
}
F
function

TestLoadJSONRejectsPrivateNetwork

Parameters

assets/assets_test.go:144-169
func TestLoadJSONRejectsPrivateNetwork(t *testing.T)

{
	const privateURL = "http://127.0.0.1/data.json"
	v1http.ClearCache(privateURL)
	t.Cleanup(func() { v1http.ClearCache(privateURL) })
	var out struct {
		V int `json:"v"`
	}
	if err := LoadJSON(privateURL, &out); err != v1http.ErrPending {
		t.Fatalf("expected ErrPending, got %v", err)
	}

	deadline := time.Now().Add(2 * time.Second)
	for {
		err := LoadJSON(privateURL, &out)
		if err != nil && err != v1http.ErrPending {
			if !strings.Contains(err.Error(), "not public") {
				t.Fatalf("unexpected private URL rejection: %v", err)
			}
			return
		}
		if time.Now().After(deadline) {
			t.Fatal("timed out waiting for private URL rejection")
		}
		time.Sleep(5 * time.Millisecond)
	}
}
S
struct

plugin

cmd/rfw/plugins/assets/assets.go:20-23
type plugin struct

Methods

Name
Method

Returns

string
func (*plugin) Name() string
{ return "assets" }
Priority
Method

Returns

int
func (*plugin) Priority() int
{ return 0 }
Build
Method

Parameters

Returns

err error
func (*plugin) Build(raw json.RawMessage) (err error)
{
	cfg := struct {
		Dir  string `json:"dir"`
		Dest string `json:"dest"`
	}{
		Dir:  "assets",
		Dest: "dist",
	}
	if len(raw) > 0 {
		if err := json.Unmarshal(raw, &cfg); err != nil {
			return fmt.Errorf("decode assets plugin config: %w", err)
		}
	}
	sourcePath, err := projectDirectory(cfg.Dir)
	if err != nil {
		return fmt.Errorf("invalid assets source: %w", err)
	}
	destinationPath, err := projectDirectory(cfg.Dest)
	if err != nil {
		return fmt.Errorf("invalid assets destination: %w", err)
	}
	p.src = sourcePath
	p.dst = destinationPath

	projectRoot, err := os.OpenRoot(".")
	if err != nil {
		return err
	}
	defer func() {
		if closeErr := projectRoot.Close(); err == nil {
			err = closeErr
		}
	}()
	if err := projectRoot.MkdirAll(destinationPath, 0o755); err != nil {
		return err
	}
	sourceRoot, err := projectRoot.OpenRoot(sourcePath)
	if err != nil {
		return err
	}
	defer func() {
		if closeErr := sourceRoot.Close(); err == nil {
			err = closeErr
		}
	}()
	targetRoot, err := projectRoot.OpenRoot(destinationPath)
	if err != nil {
		return err
	}
	defer func() {
		if closeErr := targetRoot.Close(); err == nil {
			err = closeErr
		}
	}()
	err = fs.WalkDir(sourceRoot.FS(), ".", func(path string, entry fs.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if entry.IsDir() {
			if path == "." {
				return nil
			}
			if err := targetRoot.MkdirAll(path, 0o755); err != nil {
				return err
			}
			return nil
		}
		in, err := sourceRoot.Open(path)
		if err != nil {
			return err
		}
		if err := targetRoot.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
			return errors.Join(err, in.Close())
		}
		out, err := targetRoot.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644)
		if err != nil {
			return errors.Join(err, in.Close())
		}
		_, copyErr := io.Copy(out, in)
		outCloseErr := out.Close()
		inCloseErr := in.Close()
		if err := errors.Join(copyErr, outCloseErr, inCloseErr); err != nil {
			return err
		}
		logging.Log.Info("copied file", logging.F("plugin", "assets"), logging.F("path", filepath.Join(destinationPath, path)))
		return nil
	})
	return err
}
ShouldRebuild
Method

Parameters

path string

Returns

bool
func (*plugin) ShouldRebuild(path string) bool
{
	relative, err := filepath.Rel(p.src, path)
	return err == nil && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator))
}

Fields

Name Type Description
src string
dst string
F
function

init

cmd/rfw/plugins/assets/assets.go:25-25
func init()

{ plugins.Register(&plugin{}) }
F
function

projectDirectory

Parameters

path
string

Returns

string
error
cmd/rfw/plugins/assets/assets.go:126-135
func projectDirectory(path string) (string, error)

{
	if path == "" || filepath.IsAbs(path) {
		return "", fmt.Errorf("path %q must be project-relative", path)
	}
	cleaned := filepath.Clean(path)
	if cleaned == "." || cleaned == ".." || strings.HasPrefix(cleaned, ".."+string(filepath.Separator)) {
		return "", fmt.Errorf("path %q escapes the project", path)
	}
	return cleaned, nil
}
F
function

TestBuildAndShouldRebuild

TestBuildAndShouldRebuild verifies that the assets plugin copies files and
reports rebuild requirements for files within the source directory.

Parameters

cmd/rfw/plugins/assets/assets_test.go:14-51
func TestBuildAndShouldRebuild(t *testing.T)

{
	p := &plugin{}

	tmp := t.TempDir()
	t.Chdir(tmp)
	src := "assets"
	dest := "dist"
	if err := os.MkdirAll(filepath.Join(src, "img"), 0o750); err != nil {
		t.Fatalf("mkdir: %v", err)
	}
	srcFile := filepath.Join(src, "img", "logo.png")
	if err := os.WriteFile(srcFile, []byte("data"), 0o600); err != nil {
		t.Fatalf("write src: %v", err)
	}

	cfg := struct {
		Dir  string `json:"dir"`
		Dest string `json:"dest"`
	}{Dir: src, Dest: dest}
	raw, err := json.Marshal(cfg)
	if err != nil {
		t.Fatal(err)
	}
	if err := p.Build(raw); err != nil {
		t.Fatalf("Build: %v", err)
	}

	if data, err := os.ReadFile("dist/img/logo.png"); err != nil || string(data) != "data" {
		t.Fatalf("expected copied file, got %v %q", err, data)
	}

	if !p.ShouldRebuild(srcFile) {
		t.Fatalf("expected ShouldRebuild true for %s", srcFile)
	}
	if p.ShouldRebuild(filepath.Join(tmp, "other")) {
		t.Fatalf("unexpected rebuild for unrelated file")
	}
}
F
function

TestBuildRejectsPathsOutsideProject

Parameters

cmd/rfw/plugins/assets/assets_test.go:53-70
func TestBuildRejectsPathsOutsideProject(t *testing.T)

{
	p := &plugin{}
	for _, cfg := range []struct {
		Dir  string `json:"dir"`
		Dest string `json:"dest"`
	}{
		{Dir: "../assets", Dest: "dist"},
		{Dir: "assets", Dest: "../dist"},
	} {
		raw, err := json.Marshal(cfg)
		if err != nil {
			t.Fatal(err)
		}
		if err := p.Build(raw); err == nil {
			t.Errorf("expected config %+v to be rejected", cfg)
		}
	}
}