-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #20 from akarasz/main
support durable object stubs
- Loading branch information
Showing
11 changed files
with
316 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
dist |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))) | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters