Skip to content

Commit

Permalink
feat: add worktop/durable module (#80)
Browse files Browse the repository at this point in the history
* feat: add `Bindings` type export

* fix(types): add `passThroughOnException` to FetchEvent

* chore: adjust tsconfig

* feat: begin `worktop/modules` entry

* fix(modules): restrict `Bindings` only to known keys

* feat: add `define` and `listen` helpers

* chore: add `/modules` to gitignore

* chore(example): add `basic-module` example

* chore: setup `worktop/durable` entry

* feat: add abstract `Actor` class

* fix(router): add `Durable.Namespace` to `Bindings` value

* chore(types): assert `Durable.Namespace` as binding

* chore(types): add `Durable` test w/ `Router` inside Actor

* chore: cleanup

* fix(durable): allow `storage.get` void return

* chore(types): check `D.Object` and `D.Storage` native types

* chore: add readme docs
  • Loading branch information
lukeed committed Sep 11, 2021
1 parent 129c39d commit 2877802
Show file tree
Hide file tree
Showing 13 changed files with 433 additions and 20 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ node_modules
/request
/response
/modules
/durable
/router
/utils
/ws
Expand Down
1 change: 1 addition & 0 deletions bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Promise.all([
bundle('src/request.ts', pkg.exports['./request']),
bundle('src/response.ts', pkg.exports['./response']),
bundle('src/modules.ts', pkg.exports['./modules']),
bundle('src/durable.ts', pkg.exports['./durable']),
bundle('src/crypto.ts', pkg.exports['./crypto']),
bundle('src/utils.ts', pkg.exports['./utils']),
bundle('src/cors.ts', pkg.exports['./cors']),
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
"import": "./crypto/index.mjs",
"require": "./crypto/index.js"
},
"./durable": {
"import": "./durable/index.mjs",
"require": "./durable/index.js"
},
"./request": {
"import": "./request/index.mjs",
"require": "./request/index.js"
Expand Down Expand Up @@ -67,6 +71,7 @@
"cache",
"crypto",
"cookie",
"durable",
"request",
"response",
"modules",
Expand Down
11 changes: 10 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,19 @@ The `worktop/kv` submodule contains all classes and utilities related to [Worker
The `worktop/cache` submodule contains all utilities related to [Cloudflare's Cache](https://developers.cloudflare.com/workers/learning/how-the-cache-works).

### Module: `worktop/durable`

> [View `worktop/durable` API documentation](/src/durable.d.ts)
<!-- > [View `worktop/durable` API documentation](/docs/module.durable.md) -->
The `worktop/durable` submodule includes native types for [Durable Objects](https://developers.cloudflare.com/workers/runtime-apis/durable-objects) as well as an `Actor` abstract class that provides a blueprint for authoring a Durable Object that handles WebSocket connections.

> **Note:** Durable Objects can only be used with the Module Worker format. You must integrate the `Router` with the `worktop/modules` submodule.
### Module: `worktop/modules`

> [View `worktop/modules` API documentation](/src/modules.d.ts)
<!-- > [View `worktop/cache` API documentation](/docs/module.cache.md) -->
<!-- > [View `worktop/modules` API documentation](/docs/module.modules.md) -->
The `worktop/modules` submodule includes two utilities related to the Module Workers format. Most notably, it exports a `listen` method to conform existing `Router` applications from Service Worker to Module Worker format.

Expand Down
88 changes: 88 additions & 0 deletions src/durable.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type { Bindings } from 'worktop';
import type { WebSocket } from 'worktop/ws';

export namespace Durable {
export interface Namespace {
get(id: ObjectID): Object;
idFromName(name: string): ObjectID;
idFromString(hex: string): ObjectID;
newUniqueId(options?: {
jurisdiction: 'eu';
}): ObjectID;
}

export interface ObjectID {
name?: string;
toString(): string;
}

export interface Object {
id: ObjectID;
fetch: typeof fetch;
name?: string;
}

export interface State {
id: ObjectID;
storage: Storage;
waitUntil(f: any): void;
blockConcurrencyWhile(f: any): Promise<void>;
}

export namespace Storage {
export namespace Options {
export interface Get {
/** Bypass in-memory cache management */
noCache?: boolean;
/** Opt out of race-condition protections */
allowConcurrency?: boolean;
}

export interface Put {
/** Bypass in-memory cache management */
noCache?: boolean;
/** Do not wait for disk flush */
allowUnconfirmed?: boolean;
}

export interface List extends Options.Get {
/** begin listing results from this key, inclusive */
start?: string;
/** stop listing results at this key, exclusive */
end?: string;
/** only include results if key begins with prefix */
prefix?: string;
/** if true, results given in descending lexicographic order */
reverse?: boolean;
/** maximum number of results to return */
limit?: number;
}
}
}

export interface Storage {
get<T>(key: string, options?: Storage.Options.Get): Promise<T | void>;
get<T>(keys: string[], options?: Storage.Options.Get): Promise<Map<string, T>>;

put<T>(key: string, value: T, options?: Storage.Options.Put): Promise<void>;
put<T>(entries: Record<string, T>, options?: Storage.Options.Put): Promise<void>;

delete(key: string, options?: Storage.Options.Put): Promise<boolean>;
delete(keys: string[], options?: Storage.Options.Put): Promise<number>;
deleteAll(options?: Storage.Options.Put): Promise<void>;

list<T>(options?: Storage.Options.List): Promise<Map<string, T>>;
}
}

export abstract class Actor {
public DEBUG: boolean;
constructor(state: Durable.State, bindings: Bindings);
setup?(state: Durable.State, bindings: Bindings): Promise<void> | void;

abstract receive(req: Request): Promise<Response> | Response;
fetch(input: RequestInfo, init?: RequestInit): Promise<Response>;

onconnect?(req: Request, ws: WebSocket): Promise<void> | void;
connect(req: Request): Promise<Response>;
}
54 changes: 54 additions & 0 deletions src/durable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { connect } from 'worktop/ws';
import type { Bindings } from 'worktop';
import type { WebSocket } from 'worktop/ws';
import type { Durable } from 'worktop/durable';

export abstract class Actor {
DEBUG: boolean;

constructor(state: Durable.State, bindings: Bindings) {
if (this.setup) state.blockConcurrencyWhile(this.setup(state, bindings));
this.DEBUG = false;
}

setup?(state: Durable.State, bindings: Bindings): Promise<void> | void;
onconnect?(req: Request, ws: WebSocket): Promise<void> | void;

abstract receive(req: Request): Promise<Response> | Response;

async connect(req: Request): Promise<Response> {
let error = connect(req);
if (error) return error;

let { 0: client, 1: server } = new WebSocketPair;

server.accept();

function closer() {
server.close();
}

server.addEventListener('close', closer);
server.addEventListener('error', closer);

if (this.onconnect) {
await this.onconnect(req, server);
}

return new Response(null, {
status: 101,
statusText: 'Switching Protocols',
webSocket: client,
});
}

async fetch(input: RequestInfo, init?: RequestInit): Promise<Response> {
try {
let request = new Request(input, init);
return await this.receive(request);
} catch (err) {
let msg = this.DEBUG && (err as Error).stack;
return new Response(msg || 'Error with receive', { status: 400 });
}
}
}
2 changes: 1 addition & 1 deletion src/modules.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference lib="webworker" />

import type { Bindings, CronEvent } from 'worktop';
import type { Bindings, CronEvent, ResponseHandler } from 'worktop';

export type ModuleContext = Pick<FetchEvent, 'waitUntil'>;
export type FetchContext = ModuleContext & Pick<FetchEvent, 'passThroughOnException'>;
Expand Down
3 changes: 2 additions & 1 deletion src/router.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/// <reference lib="webworker" />

import type { KV } from 'worktop/kv';
import type { Durable } from 'worktop/durable';
import type { ServerResponse } from 'worktop/response';
import type { ServerRequest, Params, Method } from 'worktop/request';

Expand Down Expand Up @@ -29,7 +30,7 @@ export interface CronEvent {
}

export interface Bindings {
[name: string]: string | CryptoKey | KV.Namespace;
[name: string]: string | CryptoKey | KV.Namespace | Durable.Namespace;
}

declare global {
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"worktop/request": ["./request.d.ts", "./request.ts"],
"worktop/response": ["./response.d.ts", "./response.ts"],
"worktop/modules": ["./modules.d.ts", "./modules.ts"],
"worktop/durable": ["./durable.d.ts", "./durable.ts"],
"worktop/cookie": ["./cookie.d.ts", "./cookie.ts"],
"worktop/utils": ["./utils.d.ts", "./utils.ts"],
"worktop/cors": ["./cors.d.ts", "./cors.ts"],
Expand Down
Loading

0 comments on commit 2877802

Please sign in to comment.