mirror of
https://github.com/syumai/workers.git
synced 2025-03-10 17:29:11 +00:00
commit
a0ec52cd49
@ -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)
|
||||
|
45
cloudflare/cache/client.go
vendored
Normal file
45
cloudflare/cache/client.go
vendored
Normal file
@ -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
cloudflare/cache/method.go
vendored
Normal file
102
cloudflare/cache/method.go
vendored
Normal file
@ -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
examples/cache/.gitignore
vendored
Normal file
1
examples/cache/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
dist
|
13
examples/cache/Makefile
vendored
Normal file
13
examples/cache/Makefile
vendored
Normal file
@ -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
examples/cache/README.md
vendored
Normal file
20
examples/cache/README.md
vendored
Normal file
@ -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
examples/cache/go.mod
vendored
Normal file
9
examples/cache/go.mod
vendored
Normal file
@ -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
examples/cache/go.sum
vendored
Normal file
4
examples/cache/go.sum
vendored
Normal file
@ -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
examples/cache/main.go
vendored
Normal file
77
examples/cache/main.go
vendored
Normal file
@ -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
examples/cache/worker.mjs
vendored
Normal file
22
examples/cache/worker.mjs
vendored
Normal file
@ -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
examples/cache/wrangler.toml
vendored
Normal file
6
examples/cache/wrangler.toml
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
name = "cache"
|
||||
main = "./worker.mjs"
|
||||
compatibility_date = "2023-02-24"
|
||||
|
||||
[build]
|
||||
command = "make build"
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user