Skip to content

Commit

Permalink
Merge pull request #20 from akarasz/main
Browse files Browse the repository at this point in the history
support durable object stubs
  • Loading branch information
syumai committed Feb 22, 2023
2 parents bcb37c4 + 04e25d2 commit c642765
Show file tree
Hide file tree
Showing 11 changed files with 316 additions and 1 deletion.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- [ ] Options for KV methods
* [ ] Cache API
* [ ] Durable Objects
- [x] Calling stubs
* [ ] D1
* [x] Environment variables

Expand Down Expand Up @@ -77,4 +78,4 @@ MIT

## Author

syumai
syumai, akarasz
133 changes: 133 additions & 0 deletions cloudflare/dostub.go
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions examples/durable-object-counter/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist
12 changes: 12 additions & 0 deletions examples/durable-object-counter/Makefile
Original file line number Diff line number Diff line change
@@ -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 changes: 40 additions & 0 deletions examples/durable-object-counter/README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 7 additions & 0 deletions examples/durable-object-counter/go.mod
Original file line number Diff line number Diff line change
@@ -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 => ../../
Empty file.
41 changes: 41 additions & 0 deletions examples/durable-object-counter/main.go
Original file line number Diff line number Diff line change
@@ -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 changes: 63 additions & 0 deletions examples/durable-object-counter/worker.mjs
Original file line number Diff line number Diff line change
@@ -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 changes: 16 additions & 0 deletions examples/durable-object-counter/wrangler.toml
Original file line number Diff line number Diff line change
@@ -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"]
1 change: 1 addition & 0 deletions internal/jsutil/jsutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ var (
Global = js.Global()
ObjectClass = Global.Get("Object")
PromiseClass = Global.Get("Promise")
RequestClass = Global.Get("Request")
ResponseClass = Global.Get("Response")
HeadersClass = Global.Get("Headers")
ArrayClass = Global.Get("Array")
Expand Down

0 comments on commit c642765

Please sign in to comment.