mirror of
https://github.com/syumai/workers.git
synced 2025-03-11 01:39:11 +00:00
commit
c642765347
@ -22,6 +22,7 @@
|
|||||||
- [ ] Options for KV methods
|
- [ ] Options for KV methods
|
||||||
* [ ] Cache API
|
* [ ] Cache API
|
||||||
* [ ] Durable Objects
|
* [ ] Durable Objects
|
||||||
|
- [x] Calling stubs
|
||||||
* [ ] D1
|
* [ ] D1
|
||||||
* [x] Environment variables
|
* [x] Environment variables
|
||||||
|
|
||||||
@ -77,4 +78,4 @@ MIT
|
|||||||
|
|
||||||
## Author
|
## Author
|
||||||
|
|
||||||
syumai
|
syumai, akarasz
|
||||||
|
133
cloudflare/dostub.go
Normal file
133
cloudflare/dostub.go
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
package cloudflare
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall/js"
|
||||||
|
|
||||||
|
"github.com/syumai/workers/internal/jsutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DurableObjectNamespace represents the namespace of the durable object.
|
||||||
|
type DurableObjectNamespace struct {
|
||||||
|
instance js.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDurableObjectNamespace returns the namespace for the `varName` binding.
|
||||||
|
//
|
||||||
|
// This binding must be defined in the `wrangler.toml` file. The method will
|
||||||
|
// return an `error` when there is no binding defined by `varName`.
|
||||||
|
func NewDurableObjectNamespace(ctx context.Context, varName string) (*DurableObjectNamespace, error) {
|
||||||
|
inst := getRuntimeContextEnv(ctx).Get(varName)
|
||||||
|
if inst.IsUndefined() {
|
||||||
|
return nil, fmt.Errorf("%s is undefined", varName)
|
||||||
|
}
|
||||||
|
return &DurableObjectNamespace{instance: inst}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IdFromName returns a `DurableObjectId` for the given `name`.
|
||||||
|
//
|
||||||
|
// https://developers.cloudflare.com/workers/runtime-apis/durable-objects/#deriving-ids-from-names
|
||||||
|
func (ns *DurableObjectNamespace) IdFromName(name string) *DurableObjectId {
|
||||||
|
id := ns.instance.Call("idFromName", name)
|
||||||
|
return &DurableObjectId{val: id}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get obtains the durable object stub for `id`.
|
||||||
|
//
|
||||||
|
// https://developers.cloudflare.com/workers/runtime-apis/durable-objects/#obtaining-an-object-stub
|
||||||
|
func (ns *DurableObjectNamespace) Get(id *DurableObjectId) (*DurableObjectStub, error) {
|
||||||
|
if id == nil || id.val.IsUndefined() {
|
||||||
|
return nil, fmt.Errorf("invalid UniqueGlobalId")
|
||||||
|
}
|
||||||
|
stub := ns.instance.Call("get", id.val)
|
||||||
|
return &DurableObjectStub{val: stub}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DurableObjectId represents an identifier for a durable object.
|
||||||
|
type DurableObjectId struct {
|
||||||
|
val js.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// DurableObjectStub represents the stub to communicate with the durable object.
|
||||||
|
type DurableObjectStub struct {
|
||||||
|
val js.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch calls the durable objects `fetch()` method.
|
||||||
|
//
|
||||||
|
// https://developers.cloudflare.com/workers/runtime-apis/durable-objects/#sending-http-requests
|
||||||
|
func (s *DurableObjectStub) Fetch(req *http.Request) (*http.Response, error) {
|
||||||
|
jsReq := toJSRequest(req)
|
||||||
|
|
||||||
|
promise := s.val.Call("fetch", jsReq)
|
||||||
|
jsRes, err := jsutil.AwaitPromise(promise)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return toResponse(jsRes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// copied from workers#request.go
|
||||||
|
func toHeader(headers js.Value) http.Header {
|
||||||
|
entries := jsutil.ArrayFrom(headers.Call("entries"))
|
||||||
|
headerLen := entries.Length()
|
||||||
|
h := http.Header{}
|
||||||
|
for i := 0; i < headerLen; i++ {
|
||||||
|
entry := entries.Index(i)
|
||||||
|
key := entry.Index(0).String()
|
||||||
|
values := entry.Index(1).String()
|
||||||
|
for _, value := range strings.Split(values, ",") {
|
||||||
|
h.Add(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// copied from workers#response.go
|
||||||
|
func toJSHeader(header http.Header) js.Value {
|
||||||
|
h := jsutil.HeadersClass.New()
|
||||||
|
for key, values := range header {
|
||||||
|
for _, value := range values {
|
||||||
|
h.Call("append", key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func toJSRequest(req *http.Request) js.Value {
|
||||||
|
jsReqOptions := jsutil.NewObject()
|
||||||
|
jsReqOptions.Set("method", req.Method)
|
||||||
|
jsReqOptions.Set("headers", toJSHeader(req.Header))
|
||||||
|
jsReqBody := js.Undefined()
|
||||||
|
if req.Body != nil {
|
||||||
|
jsReqBody = jsutil.ConvertReaderToReadableStream(req.Body)
|
||||||
|
}
|
||||||
|
jsReqOptions.Set("body", jsReqBody)
|
||||||
|
jsReq := jsutil.RequestClass.New(req.URL.String(), jsReqOptions)
|
||||||
|
return jsReq
|
||||||
|
}
|
||||||
|
|
||||||
|
func toResponse(res js.Value) (*http.Response, error) {
|
||||||
|
status := res.Get("status").Int()
|
||||||
|
promise := res.Call("text")
|
||||||
|
body, err := jsutil.AwaitPromise(promise)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
header := toHeader(res.Get("headers"))
|
||||||
|
contentLength, _ := strconv.ParseInt(header.Get("Content-Length"), 10, 64)
|
||||||
|
|
||||||
|
return &http.Response{
|
||||||
|
Status: strconv.Itoa(status) + " " + res.Get("statusText").String(),
|
||||||
|
StatusCode: status,
|
||||||
|
Header: header,
|
||||||
|
Body: io.NopCloser(strings.NewReader(body.String())),
|
||||||
|
ContentLength: contentLength,
|
||||||
|
}, nil
|
||||||
|
}
|
1
examples/durable-object-counter/.gitignore
vendored
Normal file
1
examples/durable-object-counter/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
dist
|
12
examples/durable-object-counter/Makefile
Normal file
12
examples/durable-object-counter/Makefile
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
.PHONY: dev
|
||||||
|
dev:
|
||||||
|
wrangler dev
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build:
|
||||||
|
mkdir -p dist
|
||||||
|
tinygo build -o ./dist/app.wasm -target wasm ./...
|
||||||
|
|
||||||
|
.PHONY: publish
|
||||||
|
publish:
|
||||||
|
wrangler publish
|
40
examples/durable-object-counter/README.md
Normal file
40
examples/durable-object-counter/README.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# durable object counter
|
||||||
|
|
||||||
|
This app is an exmaple of using a stub to access a direct object. The example
|
||||||
|
is based on the [cloudflare/durable-object-template](https://github.com/cloudflare/durable-objects-template)
|
||||||
|
repository.
|
||||||
|
|
||||||
|
_The durable object is written in js; only the stub is called from go!_
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
After `make publish` the trigger is `http://durable-object-counter.YOUR-DOMAIN.workers.dev`
|
||||||
|
|
||||||
|
* https://durable-object-counter.YOUR-DOMAIN.workers.dev/
|
||||||
|
* https://durable-object-counter.YOUR-DOMAIN.workers.dev/increment
|
||||||
|
* https://durable-object-counter.YOUR-DOMAIN.workers.dev/decrement
|
||||||
|
|
||||||
|
## 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
akarasz
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
7
examples/durable-object-counter/go.mod
Normal file
7
examples/durable-object-counter/go.mod
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module github.com/syumai/workers/examples/durable-object-counter
|
||||||
|
|
||||||
|
go 1.18
|
||||||
|
|
||||||
|
require github.com/syumai/workers v0.0.0
|
||||||
|
|
||||||
|
replace github.com/syumai/workers => ../../
|
0
examples/durable-object-counter/go.sum
Normal file
0
examples/durable-object-counter/go.sum
Normal file
41
examples/durable-object-counter/main.go
Normal file
41
examples/durable-object-counter/main.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/syumai/workers"
|
||||||
|
"github.com/syumai/workers/cloudflare"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
workers.Serve(&MyHandler{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type MyHandler struct {}
|
||||||
|
|
||||||
|
func (_ *MyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
COUNTER, err := cloudflare.NewDurableObjectNamespace(req.Context(), "COUNTER")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
id := COUNTER.IdFromName("A")
|
||||||
|
obj, err := COUNTER.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := obj.Fetch(req)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write([]byte("Durable object 'A' count: " + string(count)))
|
||||||
|
}
|
||||||
|
|
63
examples/durable-object-counter/worker.mjs
Normal file
63
examples/durable-object-counter/worker.mjs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Durable Object
|
||||||
|
|
||||||
|
export class Counter {
|
||||||
|
constructor(state, env) {
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle HTTP requests from clients.
|
||||||
|
async fetch(request) {
|
||||||
|
// Apply requested action.
|
||||||
|
let url = new URL(request.url);
|
||||||
|
|
||||||
|
// Durable Object storage is automatically cached in-memory, so reading the
|
||||||
|
// same key every request is fast. (That said, you could also store the
|
||||||
|
// value in a class member if you prefer.)
|
||||||
|
let value = await this.state.storage.get("value") || 0;
|
||||||
|
|
||||||
|
switch (url.pathname) {
|
||||||
|
case "/increment":
|
||||||
|
++value;
|
||||||
|
break;
|
||||||
|
case "/decrement":
|
||||||
|
--value;
|
||||||
|
break;
|
||||||
|
case "/":
|
||||||
|
// Just serve the current value.
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return new Response("Not found", {status: 404});
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't have to worry about a concurrent request having modified the
|
||||||
|
// value in storage because "input gates" will automatically protect against
|
||||||
|
// unwanted concurrency. So, read-modify-write is safe. For more details,
|
||||||
|
// see: https://blog.cloudflare.com/durable-objects-easy-fast-correct-choose-three/
|
||||||
|
await this.state.storage.put("value", value);
|
||||||
|
|
||||||
|
return new Response(value);
|
||||||
|
}
|
||||||
|
}
|
16
examples/durable-object-counter/wrangler.toml
Normal file
16
examples/durable-object-counter/wrangler.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
name = "durable-object-counter"
|
||||||
|
main = "./worker.mjs"
|
||||||
|
compatibility_date = "2022-05-13"
|
||||||
|
compatibility_flags = [
|
||||||
|
"streams_enable_constructors"
|
||||||
|
]
|
||||||
|
|
||||||
|
[build]
|
||||||
|
command = "make build"
|
||||||
|
|
||||||
|
[durable_objects]
|
||||||
|
bindings = [{name = "COUNTER", class_name = "Counter"}]
|
||||||
|
|
||||||
|
[[migrations]]
|
||||||
|
tag = "v1" # Should be unique for each entry
|
||||||
|
new_classes = ["Counter"]
|
@ -10,6 +10,7 @@ var (
|
|||||||
Global = js.Global()
|
Global = js.Global()
|
||||||
ObjectClass = Global.Get("Object")
|
ObjectClass = Global.Get("Object")
|
||||||
PromiseClass = Global.Get("Promise")
|
PromiseClass = Global.Get("Promise")
|
||||||
|
RequestClass = Global.Get("Request")
|
||||||
ResponseClass = Global.Get("Response")
|
ResponseClass = Global.Get("Response")
|
||||||
HeadersClass = Global.Get("Headers")
|
HeadersClass = Global.Get("Headers")
|
||||||
ArrayClass = Global.Get("Array")
|
ArrayClass = Global.Get("Array")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user