forked from cloudflare/workers-rs
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat: Add Support for Durable Objects (cloudflare#12)
* allow rust to access pre-existing durable object * Transition to ES Modules build-upload type to prepare for exporting a DO from Rust * updated * update * finished testing storage API * update Co-authored-by: Leo Orshansky <[email protected]>
- Loading branch information
1 parent
c4bfe84
commit 7034fa4
Showing
23 changed files
with
1,011 additions
and
125 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
**/target | ||
Cargo.lock | ||
.DS_Store | ||
.DS_Store | ||
**/worker/generated/* |
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 |
---|---|---|
@@ -1,2 +1,2 @@ | ||
[workspace] | ||
members = ["edgeworker-sys", "macros/cf", "rust-sandbox", "worker"] | ||
members = ["edgeworker-sys", "macros/cf", "macros/durable_object", "rust-sandbox", "worker"] |
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
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,10 @@ | ||
[package] | ||
name = "durable_object" | ||
version = "0.1.0" | ||
edition = "2018" | ||
|
||
[dependencies] | ||
worker = { path = "../../worker" } | ||
syn = "1.0.74" | ||
quote = "1.0.9" | ||
proc-macro2 = "1.0.28" |
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,96 @@ | ||
use proc_macro2::{Ident, TokenStream}; | ||
use quote::{ToTokens, quote}; | ||
use syn::{Error, ImplItem, Item, spanned::Spanned}; | ||
|
||
pub fn expand_macro(tokens: TokenStream) -> syn::Result<TokenStream> { | ||
let item = syn::parse2::<Item>(tokens)?; | ||
match item { | ||
Item::Impl(imp) => { | ||
let impl_token = imp.impl_token; | ||
let trai = imp.trait_.clone(); | ||
let (_, trai, _) = trai.ok_or_else(|| Error::new_spanned(impl_token, "Must be a DurableObject trait impl"))?; | ||
if !trai.segments.last().map(|x| x.ident == "DurableObject").unwrap_or(false) { | ||
return Err(Error::new(trai.span(), "Must be a DurableObject trait impl")) | ||
} | ||
|
||
let pound = syn::Token![#](imp.span()).to_token_stream(); | ||
let wasm_bindgen_attr = quote! {#pound[::wasm_bindgen::prelude::wasm_bindgen]}; | ||
|
||
|
||
let struct_name = imp.self_ty; | ||
let items = imp.items; | ||
let mut tokenized = vec![]; | ||
for item in items { | ||
let mut method = match item { | ||
ImplItem::Method(m) => m, | ||
_ => return Err(Error::new_spanned(item, "Impl block must only contain methods")) | ||
}; | ||
let tokens = match method.sig.ident.to_string().as_str() { | ||
"constructor" => { | ||
method.sig.ident = Ident::new("_constructor", method.sig.ident.span()); | ||
quote! { | ||
#pound[::wasm_bindgen::prelude::wasm_bindgen(constructor)] | ||
pub #method | ||
} | ||
}, | ||
"fetch" => { | ||
method.sig.ident = Ident::new("_fetch_raw", method.sig.ident.span()); | ||
quote! { | ||
#pound[::wasm_bindgen::prelude::wasm_bindgen(js_name = fetch)] | ||
pub fn _fetch(&mut self, req: ::edgeworker_sys::Request) -> ::js_sys::Promise { | ||
// SAFETY: | ||
// On the surface, this is unsound because the Durable Object could be dropped | ||
// while JavaScript still has possession of the future. However, | ||
// we know something that Rust doesn't: that the Durable Object will never be destroyed | ||
// while there is still a running promise inside of it, therefore we can let a reference | ||
// to the durable object escape into a static-lifetime future. | ||
let static_self: &'static mut Self = unsafe {&mut *(self as *mut _)}; | ||
|
||
::wasm_bindgen_futures::future_to_promise(async move { | ||
static_self._fetch_raw(req.into()).await.map(::edgeworker_sys::Response::from).map(::wasm_bindgen::JsValue::from) | ||
.map_err(::wasm_bindgen::JsValue::from) | ||
}) | ||
} | ||
|
||
#method | ||
} | ||
}, | ||
_ => panic!() | ||
}; | ||
tokenized.push(tokens); | ||
} | ||
Ok(quote! { | ||
#wasm_bindgen_attr | ||
impl #struct_name { | ||
#(#tokenized)* | ||
} | ||
|
||
#pound[async_trait::async_trait(?Send)] | ||
impl ::worker::durable::DurableObject for #struct_name { | ||
fn constructor(state: ::worker::durable::State, env: ::worker::Env) -> Self { | ||
Self::_constructor(state, env) | ||
} | ||
|
||
async fn fetch(&mut self, req: ::worker::Request) -> ::worker::Result<worker::Response> { | ||
self._fetch_raw(req).await | ||
} | ||
} | ||
|
||
trait __Need_Durable_Object_Trait_Impl_With_durable_object_Attribute { const MACROED: bool = true; } | ||
impl __Need_Durable_Object_Trait_Impl_With_durable_object_Attribute for #struct_name {} | ||
}) | ||
}, | ||
Item::Struct(struc) => { | ||
let tokens = struc.to_token_stream(); | ||
let pound = syn::Token![#](struc.span()).to_token_stream(); | ||
let struct_name = struc.ident; | ||
Ok(quote! { | ||
#pound[::wasm_bindgen::prelude::wasm_bindgen] | ||
#tokens | ||
|
||
const _: bool = <#struct_name as __Need_Durable_Object_Trait_Impl_With_durable_object_Attribute>::MACROED; | ||
}) | ||
}, | ||
_ => Err(Error::new(item.span(), "Durable Object macro can only be applied to structs and their impl of DurableObject trait")) | ||
} | ||
} |
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,36 @@ | ||
use cf::durable_object; | ||
use worker::{prelude::*, durable::{State}}; | ||
|
||
const ONE_HOUR: u64 = 3600000; | ||
|
||
#[durable_object] | ||
pub struct Counter { | ||
count: usize, | ||
state: State, | ||
initialized: bool, | ||
last_backup: Date | ||
} | ||
|
||
#[durable_object] | ||
impl DurableObject for Counter { | ||
fn constructor(state: worker::durable::State, _env: worker::Env) -> Self { | ||
Self { count: 0, initialized: false, state, last_backup: Date::now() } | ||
} | ||
|
||
async fn fetch(&mut self, _req: worker::Request) -> worker::Result<worker::Response> { | ||
// Get info from last backup | ||
if !self.initialized { | ||
self.initialized = true; | ||
self.count = self.state.storage().get("count").await.unwrap_or(0); | ||
} | ||
|
||
// Do a backup every hour | ||
if Date::now().as_millis() - self.last_backup.as_millis() > ONE_HOUR { | ||
self.last_backup = Date::now(); | ||
self.state.storage().put("count", self.count).await?; | ||
} | ||
|
||
self.count += 1; | ||
Response::ok(self.count.to_string()) | ||
} | ||
} |
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,48 @@ | ||
use crate::ensure; | ||
use worker::{durable::ObjectNamespace, prelude::*, Result}; | ||
|
||
pub async fn basic_test(env: &Env) -> Result<()> { | ||
let namespace: ObjectNamespace = env.get_binding("MY_CLASS")?; | ||
let id = namespace.id_from_name("A")?; | ||
let bad = env.get_binding::<ObjectNamespace>("DFSDF"); | ||
ensure!(bad.is_err(), "Invalid binding did not raise error"); | ||
|
||
let stub = id.get_stub()?; | ||
let res = stub.fetch_with_str("hello").await?.text().await?; | ||
let res2 = stub | ||
.fetch_with_request(Request::new_with_init( | ||
"hello", | ||
RequestInit::new().body(Some(&"lol".into())).method("POST"), | ||
)?) | ||
.await? | ||
.text() | ||
.await?; | ||
|
||
ensure!(res == res2, "Durable object responded wrong to 'hello'"); | ||
|
||
let res = stub.fetch_with_str("storage").await?.text().await?; | ||
let num = res | ||
.parse::<usize>() | ||
.map_err(|_| "Durable Object responded wrong to 'storage': ".to_string() + &res)?; | ||
let res = stub.fetch_with_str("storage").await?.text().await?; | ||
let num2 = res | ||
.parse::<usize>() | ||
.map_err(|_| "Durable Object responded wrong to 'storage'".to_string())?; | ||
|
||
ensure!( | ||
num2 == num + 1, | ||
"Durable object responded wrong to 'storage'" | ||
); | ||
|
||
let res = stub.fetch_with_str("transaction").await?.text().await?; | ||
let num = res | ||
.parse::<usize>() | ||
.map_err(|_| "Durable Object responded wrong to 'transaction': ".to_string() + &res)?; | ||
|
||
ensure!( | ||
num == num2 + 1, | ||
"Durable object responded wrong to 'storage'" | ||
); | ||
|
||
Ok(()) | ||
} |
Oops, something went wrong.