Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add worktop/durable module #80

Merged
merged 19 commits into from
Sep 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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