Introducing Pioche

Foreword: I have stopped working on Pioche and am reattempting the project under a new name incorporating everything I learned from this attempt. Stay tuned for tenex!

GitHub - GarrettPeake/pioche: A fully typed framework turning less work into better Cloudflare Workers
A fully typed framework turning less work into better Cloudflare Workers - GitHub - GarrettPeake/pioche: A fully typed framework turning less work into better Cloudflare Workers
Project repository

Pioche.js is a new fully typed framework for Cloudflare Workers that turns less work into better Workers. It's an early version but is still incredibly useful for fast prototypes or quick tools. If you've used Workers before, you probably had to configure your own router, then configure the handoffs between Workers and Durable Objects, then debated on how to route between different environments to minimize your bills, Pioche takes care of all of this and more!

See Intro to Workers for a quick overview of the Workers platform or the beginner-friendly Pioche Quick Start if you're ready to jump in!

Hello, World!

And welcome to Pioche, to create a new project you can use pioche-scripts with no installation through

npx pioche-scripts create <AppName>

This will create a barebones Hello, World! project which we can easily deploy to Cloudflare with npm run deploy, run locally with npm run serve , or run remotely with stdout piped to the terminal using npm run dev. All of these options use the pioche-scripts build command which handles almost all of the headaches of Workers configuration for you. It finds and exports Durable Objects, assembles an index.js entry point, performs Durable Object migrations, and creates the wrangler.toml.

In the default project, you'll find <AppName>/src/components/helloworld.ts which contains the following

import { BaseMap, GetMap, WorkerController, OutboundResponse, Session } from "pioche";

@BaseMap("")
export class HelloWorldController extends WorkerController {

    @GetMap("")
    async helloWorld(session: Session, response: OutboundResponse){
        res.body = "Hello, World!";
    }
}

This is a Controller class: a class specifying a set of logically connected methods each accessible at a specified URL. We specify that it's a WorkerController, or that the logic within the methods will execute on the worker that received the request, we could offload the logic onto a DurableObjectController or a WebsocketController as well (D1 runtime support planned). @BaseMap(<base_slug>) tells Pioche that every method in this class will share a base URL of www.example.com + <base_slug> and @GetMap(<end_slug>) creates an endpoint telling Pioche to route GET requests for www.example.com + <base_slug> + <end_slug> to this method. We can route by regex match, enable/disable endpoints and controllers, route by hostname, and route to different Durable Object IDs based on request parameters.

Now, looking back at the snippet, it should be obvious that www.example.com will have a response of Hello, World!

The Full Package

The session and response parameters are filled when the request arrives and are the backbone of all logic within a Pioche app. We can specify handlers that should execute on the session-response pair before and after each endpoint for the entire app (preHandlers and postHandlers in the Pioche config file) or scoped by endpoint (@UseBefore() and @UseAfter()).

If our endpoint is contained in a WebsocketController  we can easily add WebSocket functionality using lifecycle hooks and an adaptive message handler that deals with the underlying protocol for us.

If we need to validate JSON data we can easily describe the data structure and create assertions that we can then apply like this.

const UserView = {
    name: and(istype("string"), lenbt(5, 20)) // String >5 and <20 chars
    config: {
    	darkMode: false, // Not a required key
        browser: optional(istype("type")) // Optional, string if it exists
    }
}

const result = View(session.json(), UserView)
if(result.getStatus() === 1) console.log("Valid user data")

Finally, the two storage APIs, KV and Durable Object Storage, are natively different. KV can only store strings with optional metadata but .get(...,{type: "number"}) will perform a conversion, why not just add type as a metadata automatically on .put()? Durable Objects can store any primitive type, but doesn't support metadata! KV can only list keys, Durable Object Storage can only list pairs. So, Pioche attempts to wrangle these differences with a single, more powerful API. While the two storage mediums still use different optional parameters, KV will store any type, Durable Objects will store metadata and support expiring keys (soon), both can list keys or pairs, and both can optionally use Javascripts chaining sytnax to enable

storage.key.prop1.prop2.get();
storage.key.prop1.prop2.put("prop3", 6);
storage.key.prop1.prop2 = 5;
storage.delete("key");
delete storage.key;
delete storage.key1.prop1.prop2;

Happy Hacking!

For students with little to no budget and people just getting into web development, serverless is fantastic since it requires a minimal effort overhead and hopefully, Pioche reduces that barrier even further!

If you think this is cool, please give it a star on GitHub and share it with people you think might be interested. If you'd like to contribute, PRs are welcome.

Garrett Peake

Garrett Peake

California