Skip to content

Commit

Permalink
Merge pull request #38 from aki-0421/cache
Browse files Browse the repository at this point in the history
Add Cache API
  • Loading branch information
syumai authored Apr 28, 2023
2 parents 086a224 + 0e294e7 commit a0ec52c
Show file tree
Hide file tree
Showing 13 changed files with 307 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
- [x] Put
- [x] Delete
- [ ] Options for KV methods
* [ ] Cache API
* [x] Cache API
* [ ] Durable Objects
- [x] Calling stubs
* [x] D1 (alpha)
Expand Down
45 changes: 45 additions & 0 deletions cloudflare/cache/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package cache

import (
"syscall/js"

"github.com/syumai/workers/internal/jsutil"
)

var cache = jsutil.Global.Get("caches")

// Cache
type Cache struct {
// instance - The object that Cache API belongs to.
instance js.Value
}

// applyOptions applies client options.
func (c *Cache) applyOptions(opts []CacheOption) {
for _, opt := range opts {
opt(c)
}
}

// CacheOption
type CacheOption func(*Cache)

// WithNamespace
func WithNamespace(namespace string) CacheOption {
return func(c *Cache) {
v, err := jsutil.AwaitPromise(cache.Call("open", namespace))
if err != nil {
panic("failed to open cache")
}
c.instance = v
}
}

func New(opts ...CacheOption) *Cache {
c := &Cache{
instance: cache.Get("default"),
}
c.applyOptions(opts)

return c
}
102 changes: 102 additions & 0 deletions cloudflare/cache/method.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package cache

import (
"errors"
"net/http"
"syscall/js"

"github.com/syumai/workers/internal/jshttp"
"github.com/syumai/workers/internal/jsutil"
)

// toJSResponse converts *http.Response to JS Response
func toJSResponse(res *http.Response) js.Value {
status := res.StatusCode
if status == 0 {
status = http.StatusOK
}
respInit := jsutil.NewObject()
respInit.Set("status", status)
respInit.Set("statusText", http.StatusText(status))
respInit.Set("headers", jshttp.ToJSHeader(res.Header))

readableStream := jsutil.ConvertReaderToReadableStream(res.Body)

return jsutil.ResponseClass.New(readableStream, respInit)
}

// Put attempts to add a response to the cache, using the given request as the key.
// Returns an error for the following conditions
// - the request passed is a method other than GET.
// - the response passed has a status of 206 Partial Content.
// - Cache-Control instructs not to cache or if the response is too large.
// docs: https://developers.cloudflare.com/workers/runtime-apis/cache/#put
func (c *Cache) Put(req *http.Request, res *http.Response) error {
_, err := jsutil.AwaitPromise(c.instance.Call("put", jshttp.ToJSRequest(req), toJSResponse(res)))
if err != nil {
return err
}
return nil
}

// ErrCacheNotFound is returned when there is no matching cache.
var ErrCacheNotFound = errors.New("cache not found")

// MatchOptions represents the options of the Match method.
type MatchOptions struct {
// IgnoreMethod - Consider the request method a GET regardless of its actual value.
IgnoreMethod bool
}

// toJS converts MatchOptions to JS object.
func (opts *MatchOptions) toJS() js.Value {
if opts == nil {
return js.Undefined()
}
obj := jsutil.NewObject()
obj.Set("ignoreMethod", opts.IgnoreMethod)
return obj
}

// Match returns the response object keyed to that request.
// docs: https://developers.cloudflare.com/workers/runtime-apis/cache/#match
func (c *Cache) Match(req *http.Request, opts *MatchOptions) (*http.Response, error) {
res, err := jsutil.AwaitPromise(c.instance.Call("match", jshttp.ToJSRequest(req), opts.toJS()))
if err != nil {
return nil, err
}
if res.IsUndefined() {
return nil, ErrCacheNotFound
}
return jshttp.ToResponse(res)
}

// DeleteOptions represents the options of the Delete method.
type DeleteOptions struct {
// IgnoreMethod - Consider the request method a GET regardless of its actual value.
IgnoreMethod bool
}

// toJS converts DeleteOptions to JS object.
func (opts *DeleteOptions) toJS() js.Value {
if opts == nil {
return js.Undefined()
}
obj := jsutil.NewObject()
obj.Set("ignoreMethod", opts.IgnoreMethod)
return obj
}

// Delete removes the Response object from the cache.
// This method only purges content of the cache in the data center that the Worker was invoked.
// Returns ErrCacheNotFount if the response was not cached.
func (c *Cache) Delete(req *http.Request, opts *DeleteOptions) error {
res, err := jsutil.AwaitPromise(c.instance.Call("delete", jshttp.ToJSRequest(req), opts.toJS()))
if err != nil {
return err
}
if !res.Bool() {
return ErrCacheNotFound
}
return nil
}
1 change: 1 addition & 0 deletions examples/cache/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist
13 changes: 13 additions & 0 deletions examples/cache/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.PHONY: dev
dev:
wrangler dev

.PHONY: build
build:
mkdir -p dist
#tinygo build -o ./dist/app.wasm -target wasm ./...
tinygo build -o ./dist/app.wasm -target wasm -no-debug ./...

.PHONY: publish
publish:
wrangler publish
20 changes: 20 additions & 0 deletions examples/cache/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# [Cache](https://developers.cloudflare.com/workers/runtime-apis/cache/)

The Cache API allows fine grained control of reading and writing from the Cloudflare global network.

### Development

#### Requirements

This project requires these tools to be installed globally.

* wrangler
* tinygo

#### Commands

```
make dev # run dev server
make build # build Go Wasm binary
make publish # publish worker
```
9 changes: 9 additions & 0 deletions examples/cache/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module github.com/syumai/cache

go 1.18

require github.com/syumai/workers v0.0.0

require github.com/go-chi/chi/v5 v5.0.8 // indirect

replace github.com/syumai/workers => ../../
4 changes: 4 additions & 0 deletions examples/cache/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/syumai/workers v0.1.0 h1:z5QfQR2X+PCKzom7RodpI5J4D5YF7NT7Qwzb9AM9dgY=
github.com/syumai/workers v0.1.0/go.mod h1:alXIDhTyeTwSzh0ZgQ3cb9HQPyyYfIejupE4Z3efr14=
77 changes: 77 additions & 0 deletions examples/cache/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package main

import (
"bytes"
"fmt"
"io"
"net/http"
"time"

"github.com/syumai/workers"
"github.com/syumai/workers/cloudflare"
"github.com/syumai/workers/cloudflare/cache"
)

type responseWriter struct {
http.ResponseWriter
StatusCode int
Body []byte
}

func (rw *responseWriter) WriteHeader(statusCode int) {
rw.StatusCode = statusCode
rw.ResponseWriter.WriteHeader(statusCode)
}

func (rw *responseWriter) Write(data []byte) (int, error) {
rw.Body = append(rw.Body, data...)
return rw.ResponseWriter.Write(data)
}

func (rw *responseWriter) ToHTTPResponse() *http.Response {
return &http.Response{
StatusCode: rw.StatusCode,
Header: rw.Header(),
Body: io.NopCloser(bytes.NewReader(rw.Body)),
}
}

func handler(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
rw := responseWriter{ResponseWriter: w}
c := cache.New()

// Find cache
res, _ := c.Match(req, nil)
if res != nil {
// Set the response status code
rw.WriteHeader(res.StatusCode)
// Set the response headers
for key, values := range res.Header {
for _, value := range values {
rw.Header().Add(key, value)
}
}
rw.Header().Add("X-Message", "cache from worker")
// Set the response body
io.Copy(rw.ResponseWriter, res.Body)
return
}

// Responding
text := fmt.Sprintf("time:%v\n", time.Now().UnixMilli())
rw.Header().Set("Cache-Control", "max-age=15")
rw.Write([]byte(text))

// Create cache
cloudflare.WaitUntil(ctx, func() {
err := c.Put(req, rw.ToHTTPResponse())
if err != nil {
fmt.Println(err)
}
})
}

func main() {
workers.Serve(http.HandlerFunc(handler))
}
22 changes: 22 additions & 0 deletions examples/cache/worker.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import "../assets/polyfill_performance.js";
import "../assets/wasm_exec.js";
import mod from "./dist/app.wasm";

const go = new Go();

const readyPromise = new Promise((resolve) => {
globalThis.ready = resolve;
});

const load = WebAssembly.instantiate(mod, go.importObject).then((instance) => {
go.run(instance);
return instance;
});

export default {
async fetch(req, env, ctx) {
await load;
await readyPromise;
return handleRequest(req, { env, ctx });
}
}
6 changes: 6 additions & 0 deletions examples/cache/wrangler.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name = "cache"
main = "./worker.mjs"
compatibility_date = "2023-02-24"

[build]
command = "make build"
6 changes: 6 additions & 0 deletions internal/jshttp/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ func ToJSResponse(w *ResponseWriterBuffer) (js.Value, error) {
respInit.Set("status", status)
respInit.Set("statusText", http.StatusText(status))
respInit.Set("headers", ToJSHeader(w.Header()))
if status == http.StatusSwitchingProtocols ||
status == http.StatusNoContent ||
status == http.StatusResetContent ||
status == http.StatusNotModified {
return jsutil.ResponseClass.New(jsutil.Null, respInit), nil
}
readableStream := jsutil.ConvertReaderToReadableStream(w.Reader)
return jsutil.ResponseClass.New(readableStream, respInit), nil
}
1 change: 1 addition & 0 deletions internal/jsutil/jsutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var (
ErrorClass = Global.Get("Error")
ReadableStreamClass = Global.Get("ReadableStream")
DateClass = Global.Get("Date")
Null = js.ValueOf(nil)
)

func NewObject() js.Value {
Expand Down

0 comments on commit a0ec52c

Please sign in to comment.