Cloudflare Durable Objects + Itty Router = shorter code

Related tags

React itty-durable
Overview

Itty Durable

npm package Build Status Open Issues

TLDR; Cloudflare Durable Objects + Itty Router = much shorter code

Features

  • Removes nearly all boilerplate from Durable Objects
  • Run instance methods directly on stub (will asynchronously call the same on Durable Object)
  • Optional persistance (on change)
  • Optional created/modified timestamps

Intro

This takes the extreme stateful power of Cloudflare Durable Objects (now in open beta), but drastically cuts down on the boilerplate to use them by pairing it with the flexibility of Itty Router. Currently, the only way to communicate to durable objects (DO) is via fetch, requiring internal routing/handling of requests inside the DO, as well as building/passing the request in from a Worker or other DO in the first place. On top of that, there are a couple steps to even get the instance "stub" to work with in the first place, before you can call fetch on it.

IttyDurable offers a shortcut.

By having your durable objects extend the IttyDurable base class, it creates automatic internal routing/fetch handling via a tiny, embedded Itty Router. This allows you to ignore the initialization (from storage) step, as well as the fetch call itself from inside the DO, instead using the internal router for access/flow.

By adding in the next piece, the withDurables() middleware to the calling router (the outside Worker usually), we make this even more elegant. Now, you can typically ignore the durable object router entirely, and instead call methods (or await properties) directly on the stub itself. Under the hood, this fires a fetch that the built-in router will handle, firing the appropriate method, and passing (json-parsable) arguments in from the request.

DISCLAIMER: This is very much a "working prototype" and will be hardened over the coming weeks with the help of folks on the CF Discord group, and your feedback (in issues). API changes, polls, etc will be broadcast on the #durable-objects channel of that server, as well as on Twitter. Please follow along there (or follow me) for updates and to communicate feedback! Additionally, I'll be putting together a screencast/explanation on YouTube to show how it works - hopefully that can inspire someone else to come along and make it even better!

Installation

npm install itty-durable itty-router itty-router-extras

Example

Counter.js (your durable object class)
import { IttyDurable } from 'itty-durable'

export class Counter extends IttyDurable {
  constructor(state, env) {
    super(state, env)
    this.counter = 0
  }

  increment() {
    this.counter++
  }

  add(a, b) {
    return a + b
  }
}
Worker.js (your standard CF worker)
import { ThrowableRouter, missing, withParams } from 'itty-router-extras'
import { withDurables } from 'itty-durable'

// export the durable class, per spec
export { Counter } from './Counter'

const router = ThrowableRouter({ base: '/counter' })

router
  // add upstream middleware, allowing Durable access off the request
  .all('*', withDurables())

  // get get the durable itself... returns json response, so no need to wrap
  .get('/', ({ Counter }) => Counter.get('test').toJSON())

  // example route with multiple calls to DO
  .get('/increment-a-few-times',
    async ({ Counter }) => {
      const counter = Counter.get('test') // gets DO with id/namespace = 'test'

      // then we fire some methods on the durable... these could all be done separately.
      await Promise.all([
        counter.increment(),
        counter.increment(),
        counter.increment(),
      ])

      // and return the contents (it'll be a json response)
      return counter.toJSON()
    }
  )

  // reset the durable)
  .get('/reset', ({ Counter }) => Counter.get('test').clear())

  // will pass on unknown requests to the durable... (e.g. /counter/add/3/4 => 7)
  .get('/:action/:a?/:b?', withParams,
    ({ Counter, action, a, b }) => Counter.get('test')[action](Number(a), Number(b))
  )

  // 404 for everything else
  .all('*', () => missing('Are you sure about that?'))

// with itty, and using ES6 module syntax (required for DO), this is all you need
export default {
  fetch: router.handle
}

Interacting with it!

GET /counter/increment-a-few-times          => { counter: 3 }
GET /counter/increment-a-few-times          => { counter: 6 }
GET /counter/reset                          => { counter: 0 }
GET /counter/increment                      => { counter: 1 }
GET /counter/increment                      => { counter: 2 }
GET /counter/add/20/3                       => 23
GET /counter                                => { counter: 2 }

(more examples to come shortly, hang tight!)

Exports

IttyDurable: class

Base class to extend, with persistOnChange, but no timestamps.

createIttyDurable(options = {}): class

Factory function for defining another IttyDurable class (different base options).

withDurables(options = {})

This is the Worker middleware to put either on routes individually, up globally as an upstream route. This allows requests for the DO binding directly off the request, and simplifies even the id translation. Any durable stubs retrieved this way automatically talk to the router within IttyDurable (base class) when accessing instance methods on the stub, allowing all fetch boilerplate to be abstracted away.

Special Thanks

Big time thanks to all the fantastic developers on the Cloudflare Workers discord group, for their putting up with my constant questions, code snippets, and guiding me off the dangerous[ly flawed] path of async setters ;)

Contributors

Let's face it, in open source, these are the real heroes... improving the quality of libraries out there for everyone!

  • README tweaks, fixes, improvements: @tomByrer
Comments
  • Current status and future of this project?

    Current status and future of this project?

    I really love the idea behind itty-durable and was wondering if this is something that you guys plan to maintain in the future?

    Is it usable/stable right now for basic documented functionality?

    opened by Eduard-Hasa 8
  • Is state persistence automatic?

    Is state persistence automatic?

    It seems like state is automatically persisted, and I didn't manage to use persistent storage. I just want to make sure, if it's so then this should be in readme.

    opened by janat08 2
  • Dark mode header images

    Dark mode header images

    The README header images for all of the itty * repos make it very hard to read the second word if Github is set to dark mode.

    See:

    https://share.getcloudapp.com/QwuALvOp

    opened by grempe 2
  • Allow a Durable Object to create new unique ids and get ids from a string

    Allow a Durable Object to create new unique ids and get ids from a string

    In a durable object, the proxies overwrite the stubs on state. This will allow a durable object to still be able to create new ids and convert id strings to id objects for use in Proxy.get(id).

    const stub = this.state.env.Counter;
    const newId = stub.newUniqueId();
    const idObj = stub.idFromString(storedObjectId);
    this.state.Counter.get(newId).increment();
    this.state.Counter.get(idObj).increment();
    

    This is needed for creating fan-out systems or connection pooling like https://github.com/cloudflare/dog/.

    opened by jacwright 1
  • typo: `get get`

    typo: `get get`

    in README.md

    // get get the durable itself... returns json response, so no need to wrap
    

    should be:

    // get the durable itself... returns JSON response, so no need to wrap
    
    opened by grempe 1
  • In Counter DO example `.clear()` should be `.reset()`

    In Counter DO example `.clear()` should be `.reset()`

    I believe this line in the example code in the README:

    // reset the durable)
    .get('/reset', ({ Counter }) => Counter.get('test').clear())
    

    should be:

    // reset the durable
    .get('/reset', ({ Counter }) => Counter.get('test').reset())
    

    Please also note the removal of the extraneous ) at the end of the comment.

    Having an example that shows setting the default state would also be helpful in relation to .reset().

    opened by grempe 1
  • With Durables Parse is not working

    With Durables Parse is not working

    I am trying to set up durable objects and when I try to get the JSON to return a modified response of an object it does not give me the parsed response even though I set the parse option to true.

    opened by garretcharp 1
  • move Features to top

    move Features to top

    Allows speed readers like me to more quickly assess if this repo is worth even reading. (Seems awesome, but I look at 20 repos/day & 5 blog posts, so 'reading' is a luxury.)

    Some 'English as Second Language' folks might not understand fire = run.

    Keep up good work!

    opened by tomByrer 1
  • Itty-Durable sending empty string response causing an

    Itty-Durable sending empty string response causing an "Unhandled Promise Rejection"

    I'm using itty durable and manually creating a proxyDurable the following way:

    export const initAccountNodeByName = async (
      name: string,
      durableObject: DurableObjectNamespace
    ) => {
      const proxy = await proxyDurable(durableObject, {
        name: 'account',
        class: Account,
        parse: true,
      })
    
      const node = proxy.get(name) as Account
      return node
    }
    
    

    Everything works great but in the logs there the following errors occurs.

    YN0000: { text: '' }
    ➤ YN0000:  Trace: 
    ➤ YN0000::     at Request.json (/Users/adrianmaurer/Code/kubelt/node_modules/undici/lib/fetch/body.js:392:17)
    ➤ YN0000: :     at runNextTicks (node:internal/process/task_queues:60:5)
    ➤ YN0000: :     at processImmediate (node:internal/timers:442:9)
    ➤ YN0000: :     at process.topLevelDomainCallback (node:domain:161:15)
    ➤ YN0000::     at process.callbackTrampoline (node:internal/async_hooks:128:24)
    ➤ YN0000: :     at Request.json (/node_modules/@miniflare/core/src/standards/http.ts:333:18)
    ➤ YN0000:      at withContent (/node_modules/itty-router-extras/middleware/withContent.js:1:132)
    ➤ YN0000: :     at Object.handle (//node_modules/itty-router/dist/itty-router.min.js:1:590)
    

    note: this does not break the app but it's strange that the proxy object is returning an empty string response.

    Here is the DO:

    export default class Account extends createDurable({
      autoReturn: true,
      autoPersist: false,
    }) {
      declare state: Node.IttyDurableObjectState<Environment>
    
      async getProfile(): Promise<Profile | null> {
        const stored = await this.state.storage.get<Profile>('profile')
        return stored || null
      }
    
      async setProfile(profile: Profile): Promise<void> {
        return this.state.storage.put('profile', profile)
      }
    }
    
    opened by maurerbot 1
  • Get object id method in createDurable interface

    Get object id method in createDurable interface

    Why

    Having a default method or property on the DO proxy to reflect the DO object id is useful

    What

    Expose the object id in a property or method. Example

    myDo.$.id
    

    or

    myDo.getId()
    
    opened by maurerbot 0
  • Expose storage in state property

    Expose storage in state property

    I would be nice to access the this.state.storage object in the proxy object.

    Why?

    I've migrated to itty-durable from a standard DO. Itty-durable property proxy stores everything in a data property so I can t access my existing data properties. The storage object is there but is not exposed via types so I have to ignore the type checker and do a run time migration.

    Proposal

    A) If storage is not meant to be exposed don't store everything in a single data object. Or B) expose storage by typing state argument with DurableObjectState https://github.com/kwhitley/itty-durable/blob/v1.x/src/itty-durable.js#L20

    opened by maurerbot 3
  • Fix transformResponse

    Fix transformResponse

    The transformResponse fails for invalid JSON. Because it wasn't async, it would always return the promise from the response.json() call. Making it async allowed it to fail the JSON and move on to the next step, but because the body was already read/parsed, the response.text() would always fail. So we have to read the text first, then try and parse it as JSON. If it is JSON, return it. If not, return the text. If reading the text failed, return a response.

    opened by jacwright 0
Owner
Kevin R. Whitley
Author of apicache, itty-router, use-store, react-data-hooks, yarn-release, treeize, and more. Also I ride a OneWheel. #sumsitup
Kevin R. Whitley
A web application to search all the different countries in the world and get details about them which can include languages, currencies, population, domain e.t.c This application is built with CSS, React, Redux-Toolkit and React-Router.

A web application to search all the different countries in the world and get details about them which can include languages, currencies, population, domain e.t.c This application is built with CSS, React, Redux-Toolkit and React-Router. It also includes a theme switcher from light to dark mode.

Franklin Okolie 4 Jun 5, 2022
a more intuitive way of defining private, public and common routes for react applications using react-router-dom v6

auth-react-router is a wrapper over react-router-dom v6 that provides a simple API for configuring public, private and common routes (React suspense r

Pasecinic Nichita 12 Dec 3, 2022
Single Page Application with React, React Router, PostCSS, Webpack, Docker and Docker Compose.

spa-store Single Page Application with React, React Router, PostCSS, Webpack, Docker and Docker Compose. Contributing Feedback Roadmap Releases Check

Luis Falcon 2 Jul 4, 2022
Basic React Project with React Router, ContextAPI and Login

Getting Started with Create React App This project was bootstrapped with Create React App. Available Scripts In the project directory, you can run: np

null 26 Jan 3, 2023
Tina is an open source editor that brings visual editing into React websites. Tina empowers developers to give their teams a contextual and intuitive editing experience without sacrificing code quality.

Tina is an open-source toolkit for building content management directly into your website. Community Forum Getting Started Checkout the tutorial to ge

Tina 8.2k Jan 1, 2023
A complete set up of the React application with Typescript, Webpack 5, Babel v7, SSR, Code Splitting and HMR.

Getting Started with react-final-boilerplate Clone the code npm install You are good to go with React Application. Open http://localhost:3000/ and you

null 24 Dec 22, 2022
⚡️ Look for Covid-19 Resources, Get Vaccine Availability Notification, Complete source code for covidrescue.co.in website.

covidrescue.co.in ⚡️ Get real-time, verified leads on Oxygen, Remdesivir, ICU, Beds, Food and more based on your location. Get notifications on Vaccin

Placeholder Tech 15 Jul 10, 2022
Source code of Remotebear.

Remotebear Source code of remotebear.io. Technology & Architecture Remotebear is a NextJS web application that gathers job offers from public APIs or

Remotebear 70 Dec 6, 2022
Very simple app to decode your Vaccination Proof QR Code (such as the one provided by government of Quebec) - Compatible with SHC (Smart Health Card standard)

shc-covid19-decoder Visit simple hosted version on your phone (does NOT transmit any data, all remains in your browser) https://fproulx.github.io/shc-

François Proulx 148 Sep 23, 2022
Get updates in Telegram when a vaccination center available in your pin code. We can win Covid 🤝

Cowin Bot Get updates in Telegram when an vaccination center available in your pin code. We can win Covid ?? Commands: /start - Start the Bot /help -

Tuhin Kanti Pal 14 Oct 3, 2022
Source code for my tutorial on how to build customizable table component with React Table and Tailwind CSS.

React Table + Tailwind CSS = ❤️ Source code for my tutorial on how to build customizable table component with React Table and Tailwind CSS. Both parts

Samuel Liedtke 147 Jan 7, 2023
Further split the React Native code based on Metro build to improve performance, providing `Dll` and `Dynamic Imports` features

React-Native Code Splitting Further split the React Native code based on Metro build to improve performance, providing Dll and Dynamic Imports feature

Wuba 126 Dec 29, 2022
Finished code and notes from EFA bonus class on building a React project without create-react-app

React From Scratch Completed Code This is the completed code for the EFA bonus class on building a React project from scratch. Included are also markd

Conor Broaders 3 Oct 11, 2021
This hook allows you to isolate and manage the state within the component, reducing rendering operations and keeping the source code concise.

React Hook Component State This hook allows you to isolate and manage the state within the component, reducing rendering operations and keeping the so

Yongwoo Jung 2 May 15, 2022
This is not my code, just trained with "From Scratch - Développement Web" youtube video

React-Countries-API DISCLAIMER FR : Ceci n'est pas mon code, je me suis juste entraîné à partir de la vidéo de From Scratch - Développement Web ! EN :

LejusVDP 1 Jan 4, 2022
Any code using props of Express.

Some projects to test knowledge with express and nodejs SESSIONS AND COOCKIES Login Basic example use session, redirect and file manipulation. Views B

Mateus Nicolau 1 Jan 7, 2022
Using Ethereum Smart Contracts to verify any user's vaccination via Identification Number or QR Code.

Covid-Vaccine-Verification-Blockchain Using Ethereum Smart Contracts to verify any user's vaccination via Identification Number or QR Code. Requiremen

Zaynab Batool Reza 4 May 14, 2022
This is a code repository for the corresponding video tutorial. In this video, we're going to build a Modern UI/UX Restaurant Landing Page Website

Restaurant Landing Page Live Site Stay up to date with new projects New major projects coming soon, subscribe to the mailing list to stay up to date h

Adrian Hajdin - JavaScript Mastery 843 Jan 4, 2023
React component library for displaying code samples with syntax highlighting!!

react-code-samples usage example: import {} from 'react-code-samples'; import 'highlight.js/styles/default.css'; // or use another highlight js style

Pranav Teegavarapu 8 Jan 3, 2022