-
-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
105 additions
and
115 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,155 +1,145 @@ | ||
# ![itty-durable](https://user-images.githubusercontent.com/865416/175660491-4f428e41-47f5-4d43-92d3-02ce29309878.png) | ||
<br /> | ||
|
||
[![npm package][npm-image]][npm-url] | ||
![Build Status](https://github.com/kwhitley/itty-router/actions/workflows/verify.yml/badge.svg) | ||
[![Open Issues][issues-image]][issues-url] | ||
<a href="https://github.com/kwhitley/itty-durable" target="\_parent"> | ||
<img alt="" src="https://img.shields.io/github/stars/kwhitley/itty-durable.svg?style=social&label=Star" /> | ||
</a> | ||
<a href="https://twitter.com/kevinrwhitley" target="\_parent"> | ||
<img alt="" src="https://img.shields.io/twitter/follow/kevinrwhitley.svg?style=social&label=Follow" /> | ||
<p> | ||
<a href="https://itty.dev/itty-durable" target="_blank"> | ||
<img src="https://github.com/kwhitley/itty-time/assets/865416/6bb35cab-2f99-4d31-bcc2-c3c37112e4c7" alt="durable" height="120" /> | ||
</a> | ||
</p> | ||
|
||
[![Version](https://img.shields.io/npm/v/itty-time.svg?style=flat-square)](https://npmjs.com/package/itty-durable) | ||
![npm bundle size](https://img.shields.io/bundlephobia/minzip/itty-durable%40next?style=flat-square) | ||
<!--[![Coverage Status](https://img.shields.io/coveralls/github/kwhitley/itty-durable?style=flat-square)](https://coveralls.io/github/kwhitley/itty-durable)--> | ||
<!--[![Issues](https://img.shields.io/github/issues/kwhitley/itty-durable?style=flat-square)](https://coveralls.io/github/kwhitley/itty-durable)--> | ||
[![Discord](https://img.shields.io/discord/832353585802903572?label=Discord&logo=Discord&style=flat-square&logoColor=fff)](https://discord.gg/53vyrZAu9u) | ||
|
||
### [v3 Documentation - NOT YET AVAILABLE](https://itty.dev/itty-durable) | [Discord](https://discord.gg/53vyrZAu9u) | ||
|
||
--- | ||
|
||
Simplifies usage of [Cloudflare Durable Objects](https://blog.cloudflare.com/introducing-workers-durable-objects/), allowing **lightweight object definitions** and **direct access** to object methods from within Workers (no need for request building/handling). | ||
Tiny (~400 bytes) wrapper for [Cloudflare Durable Objects](https://blog.cloudflare.com/javascript-native-rpc), adding the following functionality to the native RPC implementation: | ||
|
||
## Features | ||
- Removes nearly all boilerplate from writing **and** using Durable Objects. | ||
- Optional automatic non-blocking persistance layer | ||
- Optionally return contents from methods without explicit return (convenience feature) | ||
- Control how contents of object looks to outside requests | ||
- Control exactly what, if anything, is persisted | ||
- Already being used in production on high-availability/throughput apps like the [Retheme](http://retheme.org/) browser extension! | ||
1. Auto persists properties to storage | ||
2. Optionally define your own external store (e.g. R2, KV, supabase, any async service/API, etc) | ||
3. Use with *any* router/framework... itty, Hono, etc | ||
|
||
*<b>Bonus</b> - Middleware is included for itty-router and Hono to allow easier accessing of stubs by string identifier (simplifies access from Worker). Documentation to come...* | ||
|
||
## Disclaimer | ||
_This is a `next` tag release, for playtesting/confirming the final public version. While I've tried to cover the bases, there is not yet test coverage (until the API is finalized), and the API may change as discussions/feedback unfolds. Use at your own risk!_ | ||
|
||
## Installation | ||
``` | ||
npm install itty-router itty-durable | ||
npm install itty-durable@next | ||
``` | ||
|
||
## Example | ||
##### Counter.js (your Durable Object class) | ||
```js | ||
import { createDurable } from 'itty-durable' | ||
```ts | ||
import { IttyDurable } from 'itty-durable' | ||
|
||
export class Counter extends createDurable({ autoReturn: true }) { | ||
constructor(state, env) { | ||
super(state, env) | ||
|
||
// anything defined here is only used for initialization (if not loaded from storage) | ||
this.counter = 0 | ||
} | ||
export class Counter extends IttyDurable { | ||
// this property will be persisted | ||
value = 20 | ||
|
||
// Because this function does not return anything, it will return the entire contents | ||
// Example: { counter: 1 } | ||
increment() { | ||
this.counter++ | ||
} | ||
increment(by: number = 1) { | ||
this.value += by | ||
|
||
// Any explicit return will honored, despite the autoReturn flag. | ||
// Note that any serializable params can passed through from the Worker without issue. | ||
add(a, b) { | ||
return a + b | ||
return this.$props() // optionally return the props | ||
} | ||
} | ||
``` | ||
|
||
##### Worker.js (your CF Worker function) | ||
```js | ||
import { ThrowableRouter, missing, withParams } from 'itty-router-extras' | ||
import { withDurables } from 'itty-durable' | ||
|
||
// export the durable class, per spec | ||
export { Counter } from './Counter' | ||
## How it Works | ||
|
||
const router = ThrowableRouter({ base: '/counter' }) | ||
Under the hood, `IttyDurable` (which directy extends `DurableObject`) returns a Proxy to itself. When methods on your Durable Object are called, it intercepts these requests to do the following lifecycle steps: | ||
|
||
router | ||
// add upstream middleware, allowing Durable access off the request | ||
.all('*', withDurables()) | ||
1. Sync props from storage if not already done. Thanks to RPC being fully async, we can safely await this, allowing slow requests from any data source. | ||
2. Execute the original method call. | ||
3. Register a debounced timer (default delay is 2000ms, subject to change) to persist the props back to storage. This prevents rapid method executions from writing more than necessary to storage. When action on the DO stops, the write executes well before the DO expires. | ||
|
||
// get the durable itself... returns json response, so no need to wrap | ||
.get('/', ({ Counter }) => Counter.get('test').toJSON()) | ||
# API | ||
|
||
// By using { autoReturn: true } in createDurable(), this method returns the contents | ||
.get('/increment', ({ Counter }) => Counter.get('test').increment()) | ||
The API of `IttyDurable` is intentionally minimalist. In fact, we only expose a handful of properties/methods: | ||
|
||
// you can pass any serializable params to a method... (e.g. /counter/add/3/4 => 7) | ||
.get('/add/:a?/:b?', withParams, | ||
({ Counter, a, b }) => Counter.get('test').add(Number(a), Number(b)) | ||
) | ||
### `$` - memory/temp properties | ||
The `$` property on your DO defaults to a blank object, and is designed to house any memory-only properties you want. This property, and all contents within it, is specifically excluded from storage writes. | ||
|
||
// reset the durable | ||
.get('/reset', ({ Counter }) => Counter.get('test').reset()) | ||
```ts | ||
export class Counter extends IttyDurable { | ||
// this property will be persisted | ||
value = 20 | ||
|
||
// 404 for everything else | ||
.all('*', () => missing('Are you sure about that?')) | ||
// but this will not | ||
$ = { | ||
history: [1,2,3] | ||
} | ||
|
||
// with itty, and using ES6 module syntax (required for DO), this is all you need | ||
export default { | ||
fetch: router.handle | ||
increment(by: number = 1) { | ||
this.value += by | ||
this.$.history.push(by) | ||
} | ||
} | ||
|
||
/* | ||
Example Interactions: | ||
GET /counter => { counter: 0 } | ||
GET /counter/increment => { counter: 1 } | ||
GET /counter/increment => { counter: 2 } | ||
GET /counter/increment => { counter: 3 } | ||
GET /counter/reset => { counter: 0 } | ||
GET /counter/add/20/3 => 23 | ||
*/ | ||
``` | ||
|
||
## How it Works | ||
This library works via a two part process: | ||
### `$store` - swappable external storage | ||
The `$store` property can be used to swap in your own external storage. The required interface is simple, consisting of a get/put method pair. These functions can be sync/async - as they'll be awaited regardless. To use the original DO as `this` within your functions (e.g. to access `this.ctx` or bindings within `this.env`), you **must** write these in function syntax (shown below), rather than arrow functions. If you don't need `this`, then either will work. | ||
|
||
1. First of all, we create a base class for your Durable Objects to extend (through `createDurable()`). This embeds the persistance layer, a few convenience functions, and most importantly, a tiny internal [itty-router](https://www.npmjs.com/package/itty-router) to handle fetch requests. Using this removes the boilerplate from your objects themselves, allowing them to be **only** business logic. | ||
```ts | ||
const CustomStore = { | ||
get() { | ||
// return some data here | ||
}, | ||
put(value: any) { | ||
// handle storage of value | ||
} | ||
} | ||
|
||
2. Next, we expose the `withDurables()` middleware for use within your Workers (it is designed for drop-in use with [itty-router](https://www.npmjs.com/package/itty-router), but should work with virtually any typical Worker router). This embeds proxied stubs (translation: "magic stubs") into the Request. Using these stubs, you can call methods on the Durable Object directly, rather than manually creating fetch requests to do so (that's all handled internally, communicating with the embedded router within the Durable Objects themselves). | ||
|
||
## Installation | ||
export class Counter extends IttyDurable { | ||
$store = CustomStore | ||
|
||
// ... | ||
} | ||
``` | ||
npm install itty-durable | ||
|
||
To give you a better idea of a store implementation, here's the actual default store (writes to a single key in default DO storage): | ||
```ts | ||
$store: DurableStore = { | ||
get() { | ||
return this.ctx.storage.get('v') | ||
}, | ||
put(value: any) { | ||
this.ctx.storage.put('v', value) | ||
} | ||
} | ||
``` | ||
|
||
# API | ||
### `$persist(delay = 0)` - manually persist to storage | ||
The `$persist()` method is automatically called internally when methods are executed, but you can always call it yourself manually if you want an immediate (or delayed) write. | ||
|
||
### `createDurable(options?: object): class` | ||
Factory function to create the IttyDurable class (with options) for your Durable Object to extend. | ||
```ts | ||
export class Counter extends IttyDurable { | ||
value = 20 | ||
|
||
| Option | Type(s) | Default | Description | | ||
| --- | --- | --- | --- | | ||
| **autoPersist** | `boolean` | false | By default, all contents are stored in-memory only, and are cleared when the DO evacuates from memory (unless explicitly asked to persist). By setting this to `true`, each request to the DO through the stub will persist the contents automatically. | ||
| **autoReturn** | `boolean` | false | If set to `true`, methods without an explicit return will return the contents of the object itself (as controlled through the `toJSON()` method). This method is overridable for custom payload shaping. | ||
increment(by: number = 1) { | ||
this.value += by | ||
|
||
### `withDurables(options?: object): function` | ||
Highly-recommended middleware to embed itty-durable stubs into the request. Using these stubs allows you to skip manually creating/sending requests or handling response parsing. | ||
this.$persist() // calls persist with 0 delay | ||
this.$persist(10000) // calls persist with 10 second delay | ||
} | ||
} | ||
``` | ||
|
||
| Option | Type(s) | Default | Description | | ||
| --- | --- | --- | --- | | ||
| **parse** | `boolean` | false | By default, the stub methods return a Promise to the Response from the Durable Object itself. This is great if you're just passing the response along and don't want to modify it. To take more control, setting this to `true` will instead return a Promise to the parsed JSON contents instead. To then respond to requests, you would have to handle building of a JSON Response yourself (e.g. `json()` within itty-router-extras). | ||
### `$props()` - return the DO properties | ||
The `$props()` method is used to simplify the extraction of your own properties, including the contents within the memory-only `$` object. This is a helpful return for your functions that are called from a Worker, if they need to see/return the contents of the DO after exection. This function naturally omits the `env`, `ctx, `$store` properties on the DO. | ||
|
||
--- | ||
```ts | ||
export class Counter extends IttyDurable { | ||
value = 20 | ||
foo = 'bar' | ||
|
||
[twitter-image]:https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fitty-durable | ||
[logo-image]:https://user-images.githubusercontent.com/865416/114285361-2bd3e180-9a1c-11eb-8386-a2e9f4383d43.png | ||
[gzip-image]:https://img.shields.io/bundlephobia/minzip/itty-durable | ||
[gzip-url]:https://bundlephobia.com/result?p=itty-durable | ||
[issues-image]:https://img.shields.io/github/issues/kwhitley/itty-durable | ||
[issues-url]:https://github.com/kwhitley/itty-durable/issues | ||
[npm-image]:https://img.shields.io/npm/v/itty-durable.svg | ||
[npm-url]:http://npmjs.org/package/itty-durable | ||
[travis-image]:https://travis-ci.org/kwhitley/itty-durable.svg?branch=v0.x | ||
[travis-url]:https://travis-ci.org/kwhitley/itty-durable | ||
[david-image]:https://david-dm.org/kwhitley/itty-durable/status.svg | ||
[david-url]:https://david-dm.org/kwhitley/itty-durable | ||
[coveralls-image]:https://coveralls.io/repos/github/kwhitley/itty-durable/badge.svg?branch=v0.x | ||
[coveralls-url]:https://coveralls.io/github/kwhitley/itty-durable?branch=v0.x | ||
|
||
## 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](https://github.com/tomByrer) | ||
increment(by: number = 1) { | ||
this.value += by | ||
|
||
return this.$props() // { $: {}, value: 21, foo: 'bar' } | ||
} | ||
} | ||
``` |