Skip to content

Commit

Permalink
feat: add durable storage MVP
Browse files Browse the repository at this point in the history
  • Loading branch information
MasterPtato committed May 13, 2024
1 parent 911da30 commit e4cabfa
Show file tree
Hide file tree
Showing 16 changed files with 257 additions and 13 deletions.
2 changes: 1 addition & 1 deletion artifacts/runtime_archive.json

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions deno.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"version": "3",
"packages": {
"specifiers": {
"npm:@prisma/adapter-pg@^5.12.0": "npm:@prisma/[email protected][email protected]",
"npm:@prisma/adapter-pg@^5.9.1": "npm:@prisma/[email protected][email protected]",
"npm:@rivet-gg/esbuild-plugin-polyfill-node@^0.4.0": "npm:@rivet-gg/[email protected][email protected]",
"npm:@types/node": "npm:@types/[email protected]",
Expand Down Expand Up @@ -146,6 +147,14 @@
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dependencies": {}
},
"@prisma/[email protected][email protected]": {
"integrity": "sha512-7AlihIeGvEuuGlZcjv66KAcz/xiu3pqgZzihh+447CDgf8eC+YtuT4tKgewyuYVWDAFb/U1SefgnAjcjAxRrXg==",
"dependencies": {
"@prisma/driver-adapter-utils": "@prisma/[email protected]",
"pg": "[email protected]",
"postgres-array": "[email protected]"
}
},
"@prisma/[email protected][email protected]": {
"integrity": "sha512-0RhnB1sqLmSzilbIQS75YE5qTNotlGvLWhTG7cp+F59VJdQ5sWWXZ1j2W4Tn+AMb116Yp4AuFe0vggW2f+ujiQ==",
"dependencies": {
Expand All @@ -154,6 +163,16 @@
"postgres-array": "[email protected]"
}
},
"@prisma/[email protected]": {
"integrity": "sha512-699iqlEvzyCj9ETrXhs8o8wQc/eVW+FigSsHpiskSFydhjVuwTJEfj/nIYqTaWFYuxiWQRfm3r01meuW97SZaQ==",
"dependencies": {}
},
"@prisma/[email protected]": {
"integrity": "sha512-SaimwvGuvXJTsWH+FOfl7PlkZZlPiRGeMEchC0kvB08Xw9B3CLYo/Q7zaY5Fp+u5D8asrpcFQmCjXEgDvOmPkg==",
"dependencies": {
"@prisma/debug": "@prisma/[email protected]"
}
},
"@prisma/[email protected]": {
"integrity": "sha512-1h1+6kU3PfsE2CvQoU49Jt7yrXybU0C/H/+W8Bh0pJefMAbTY8KoWssYXgwQp1PUKBtdh1GDQsMf9ojFeQ0RWg==",
"dependencies": {
Expand Down Expand Up @@ -725,7 +744,9 @@
},
"redirects": {
"https://esm.sh/@neondatabase/serverless@^0.9.0": "https://esm.sh/@neondatabase/[email protected]",
"https://esm.sh/@neondatabase/serverless@^0.9.3": "https://esm.sh/@neondatabase/[email protected]",
"https://esm.sh/@prisma/adapter-neon@^5.10.2": "https://esm.sh/@prisma/[email protected]",
"https://esm.sh/@prisma/adapter-neon@^5.13.0": "https://esm.sh/@prisma/[email protected]",
"https://esm.sh/ajv-formats@^2.1.1": "https://esm.sh/[email protected]",
"https://esm.sh/ajv@^8.12.0": "https://esm.sh/[email protected]",
"https://esm.sh/pg@^8.11.3": "https://esm.sh/[email protected]",
Expand Down Expand Up @@ -2550,14 +2571,20 @@
"https://deno.land/x/[email protected]/cache.ts": "89eea5f3ce6035a1164b3e655c95f21300498920575ade23161421f5b01967f4",
"https://deno.land/x/[email protected]/loader.ts": "d98d195a715f823151cbc8baa3f32127337628379a02d9eb2a3c5902dbccfc02",
"https://esm.sh/@neondatabase/[email protected]": "3007f35c946e1ce273f644f2bfee27a9de48d1fbe332dab9d53566d12080e22d",
"https://esm.sh/@neondatabase/[email protected]": "44d18e016ebf3ce55f3e87f8f65976f6e8ff0624faafff1625bf6dd5863d21c6",
"https://esm.sh/@prisma/[email protected]": "0139f84d719977a05264596fef6bb34e4d0e9ddf15f73d6fd9ec6ff203a8d432",
"https://esm.sh/@prisma/[email protected]": "5b223ec54e4893e0191797043fda7eedbea8b3442f683e55a48633c5bad4b263",
"https://esm.sh/[email protected]": "575b3830618970ddc3aba96310bf4df7358bb37fcea101f58b36897ff3ac2ea7",
"https://esm.sh/[email protected]": "cc1a73af661466c7f4e6a94d93ece78542d700f2165bdb16a531e9db8856c5aa",
"https://esm.sh/v135/@neondatabase/[email protected]/denonext/serverless.mjs": "a45acb984910225aa035c9787133d117fb585d6d62536238e135580cf6a0a695",
"https://esm.sh/v135/@neondatabase/[email protected]/denonext/serverless.mjs": "170ea3600a913c22aa959044968d73c6a214f17ce6ea60ce2002d4e9225f2491",
"https://esm.sh/v135/@neondatabase/[email protected]/denonext/serverless.mjs": "5351e9d60196cfbec4e83de6a91fd2878c201a9d79262248fd615373725eb061",
"https://esm.sh/v135/@prisma/[email protected]/denonext/adapter-neon.mjs": "3639e2b49242e394582e1e36623a62ab75c60a487c502bc6d9665944531728dc",
"https://esm.sh/v135/@prisma/[email protected]/denonext/adapter-neon.mjs": "964ca03df72d71669e519581e826dce6c21208e2530b2be6c1b283d4efa1542a",
"https://esm.sh/v135/@prisma/[email protected]/denonext/debug.mjs": "cab0a6f1eb5df296ddf9b9590519e6bdc851bfad482961b7bd25e5bfe1cc428c",
"https://esm.sh/v135/@prisma/[email protected]/denonext/debug.mjs": "f9422affb4aed3f33c884f52160531fabb7f3bd311ad15e99be3796640533e6f",
"https://esm.sh/v135/@prisma/[email protected]/denonext/driver-adapter-utils.mjs": "2266fc26eebd8821b502bb0ada2488062259b3b4d8188cd5692e13f14caa174c",
"https://esm.sh/v135/@prisma/[email protected]/denonext/driver-adapter-utils.mjs": "db5e5076ba2dab34d8ccb96dc211dbab04fa3a3d25ef648a34689c85e8f34612",
"https://esm.sh/v135/[email protected]/denonext/ajv-formats.mjs": "06092e00b42202633ae6dab4b53287c133af882ddb14c6707277cdb237634967",
"https://esm.sh/v135/[email protected]/denonext/ajv.mjs": "4645df9093d0f8be0e964070a4a7aea8adea06e8883660340931f7a3f979fc65",
"https://esm.sh/v135/[email protected]/denonext/dist/compile/codegen.js": "d981238e5b1e78217e1c6db59cbd594369279722c608ed630d08717ee44edd84",
Expand Down
10 changes: 7 additions & 3 deletions src/build/entrypoint.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { resolve } from "../deps.ts";
import { Project } from "../project/mod.ts";
import { genRegistryMapPath, genRuntimeModPath, genRuntimePath } from "../project/project.ts";
import { genRegistryMapPath, genRuntimeDurablePath, genRuntimeModPath, genRuntimePath } from "../project/project.ts";
import { CommandError } from "../error/mod.ts";
import { autoGenHeader } from "./misc.ts";
import { BuildOpts, DbDriver, Runtime } from "./mod.ts";
import { dedent } from "./deps.ts";

export async function generateEntrypoint(project: Project, opts: BuildOpts) {
const runtimeModPath = genRuntimeModPath(project);
const runtimeDurablePath = genRuntimeDurablePath(project, opts);
const registryMapPath = genRegistryMapPath(project);

// Generate module configs
Expand All @@ -24,8 +25,8 @@ export async function generateEntrypoint(project: Project, opts: BuildOpts) {
} else if (opts.dbDriver == DbDriver.NeonServerless) {
imports += `
// Import Prisma serverless adapter for Neon
import * as neon from "https://esm.sh/@neondatabase/serverless@^0.9.0";
import { PrismaNeonHTTP } from "https://esm.sh/@prisma/adapter-neon@^5.10.2";
import * as neon from "https://esm.sh/@neondatabase/serverless@^0.9.3";
import { PrismaNeonHTTP } from "https://esm.sh/@prisma/adapter-neon@^5.13.0";
`;
}

Expand Down Expand Up @@ -110,6 +111,9 @@ export async function generateEntrypoint(project: Project, opts: BuildOpts) {
});
}
}
// Export durable object binding
export { __GlobalDurableObject } from "${runtimeDurablePath}";
`;
}

Expand Down
6 changes: 5 additions & 1 deletion src/build/gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ import { Module, moduleGenPath, Project, Script, scriptGenPath, testGenPath, typ
import { genRuntimeModPath } from "../project/project.ts";
import { autoGenHeader } from "./misc.ts";
import { camelify, pascalify } from "../types/case_conversions.ts";
import { genRegistryMapPath } from "../project/project.ts";
import { genRegistryMapPath, genRuntimeDurablePath } from "../project/project.ts";
import { hasUserConfigSchema } from "../project/module.ts";
import { BuildOpts } from "./mod.ts";

export async function compileModuleHelper(
project: Project,
module: Module,
opts: BuildOpts,
) {
const helperPath = moduleGenPath(project, module);
const runtimePath = relative(dirname(helperPath), genRuntimeModPath(project));
const runtimeDurablePath = relative(dirname(helperPath), genRuntimeDurablePath(project, opts));

// Generate source
let dbImports = "";
Expand Down Expand Up @@ -41,6 +44,7 @@ export async function compileModuleHelper(
*/
export type Empty = Record<string, never>;
export { RuntimeError } from "${runtimePath}";
export { Durable } from "${runtimeDurablePath}";
export type ModuleContext = ModuleContextInner<
RegistryTypeInner,
RegistryCamelTypeInner,
Expand Down
2 changes: 1 addition & 1 deletion src/build/plan/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export async function planModuleBuild(
},
},
async build() {
await compileModuleHelper(project, module);
await compileModuleHelper(project, module, opts);
},
});

Expand Down
2 changes: 2 additions & 0 deletions src/build/plan/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ export async function planProjectBuild(
// Wasm must be loaded as a separate file manually, cannot be bundled
"*.wasm",
"*.wasm?module",
// This import only exists when running on cloudflare
"cloudflare:workers",
],
bundle: true,
minify: true,
Expand Down
12 changes: 12 additions & 0 deletions src/project/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { validateIdentifier } from "../types/identifiers/mod.ts";
import { Casing } from "../types/identifiers/defs.ts";
import { loadDefaultRegistry } from "./registry.ts";
import { UserError } from "../error/mod.ts";
import { BuildOpts, Runtime } from "../build/mod.ts";

export interface Project {
path: string;
Expand Down Expand Up @@ -241,6 +242,17 @@ export function genRuntimeModPath(project: Project): string {
return resolve(project.path, "_gen", "runtime", "src", "runtime", "mod.ts");
}

export function genRuntimeDurablePath(project: Project, opts: BuildOpts): string {
return resolve(
project.path,
"_gen",
"runtime",
"src",
"runtime",
opts.runtime == Runtime.Cloudflare ? "durable_cf.ts" : "durable.ts",
);
}

export function genRegistryMapPath(project: Project): string {
return resolve(project.path, "_gen", "registryMap.ts");
}
Expand Down
8 changes: 5 additions & 3 deletions src/runtime/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,11 @@ export class Context<RegistryT, RegistryCamelT> {
cause.enrich(this.runtime, this);
throw cause;
} else {
const error = new RuntimeError("INTERNAL_ERROR", { cause });
error.enrich(this.runtime, this);
throw error;
throw cause;

// const error = new RuntimeError("INTERNAL_ERROR", { cause });
// error.enrich(this.runtime, this);
// throw error;
}
}
}
Expand Down
71 changes: 71 additions & 0 deletions src/runtime/durable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// This file is only imported when the runtime is `Deno`. See `durable_cf.ts` in the same directory.

const ENCODER = new TextEncoder();
const MEMORY_DURABLE_STORAGE: Map<string, {
durable: Durable;
storage: Map<string, any>;
}> = new Map();

export class Durable {
storage: DurableStorage;

constructor(public id: string) {
this.storage = new DurableStorage(id);
}

// Generics allow this function to match the type of whatever sub-class this function is called for
static async for<T extends typeof Durable>(this: T, name: string): Promise<InstanceType<T>> {
const id = await hash(name);

// Fetch from global storage if exists
const storageEntry = MEMORY_DURABLE_STORAGE.get(id);
if (storageEntry != undefined) {
return storageEntry.durable as InstanceType<T>;
} else {
// Create a new durable
const durable = new this(id);

MEMORY_DURABLE_STORAGE.set(id, {
durable,
storage: new Map(),
});

return durable as InstanceType<T>;
}
}
}

// In-memory storage implementation
class DurableStorage {
constructor(private id: string) {}

async get(keys: string | string[]): Promise<any | any[]> {
if (keys instanceof Array) {
return keys.map((key) => MEMORY_DURABLE_STORAGE.get(this.id)!.storage.get(key));
} else {
return MEMORY_DURABLE_STORAGE.get(this.id)!.storage.get(keys);
}
}

async put(key: string, value: any) {
MEMORY_DURABLE_STORAGE.get(this.id)!.storage.set(key, value);
}

async delete(keys: string | string[]) {
if (keys instanceof Array) {
for (const key of keys) {
MEMORY_DURABLE_STORAGE.get(this.id)!.storage.delete(key);
}
} else {
MEMORY_DURABLE_STORAGE.get(this.id)!.storage.delete(keys);
}
}
}

async function hash(input: string) {
const data = ENCODER.encode(input);
const hash = await crypto.subtle.digest("SHA-256", data);
const hashString = Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");

return hashString;
}
74 changes: 74 additions & 0 deletions src/runtime/durable_cf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// This file is only imported when the runtime is `Cloudflare`. See `durable.ts` in the same directory.

declare module "cloudflare:workers" {
// TODO: Replace with imported types, maybe from denoflare
export type DurableObjectCtx = {
storage: DurableObjectStorage;
};
export class DurableObjectStorage {
get(keys: string | string[]): Promise<any | any[]>;
put(key: string, value: any): Promise<void>;
delete(keys: string | string[]): Promise<void>;
}
export class DurableObjectHandle {
idFromName(name: string): DurableObjectId;
get(id: DurableObjectId): DurableObject;
}
export type DurableObjectId = any;
export type DurableObjectEnv = any;

export class DurableObject {
constructor(_ctx: DurableObjectCtx, _env: DurableObjectEnv);
}
}

import {
DurableObject,
DurableObjectCtx,
DurableObjectEnv,
DurableObjectHandle,
DurableObjectStorage,
} from "cloudflare:workers";

export class Durable {
static tag: string = "durable";
storage: DurableObjectStorage;

constructor(public id: string, storage: DurableObjectStorage) {
this.storage = storage;
}

// Generics allow this function to match the type of whatever sub-class this function is called for
static async for<T extends typeof Durable>(this: T, name: string): Promise<InstanceType<T>> {
// Get the global durable object from cloudflare env
const doHandle = Deno.env.get("__GLOBAL_DURABLE_OBJECT") as any as DurableObjectHandle;

// IMPORTANT: Any changes to this pattern will cause durable objects created with the previous pattern
// to be "forgotten".
// Get a durable object id based on the name of the class implementing `Durable` and the provided
// name. This effectively namespaces the single durable object into any number of durable objects.
const id = doHandle.idFromName(`%%${this.tag}%%${name}`);

const doStub = doHandle.get(id) as __GlobalDurableObject;

throw Error(`${(this as any).tag}\n${doStub.constructor.name}\n${doStub.foo}\n${Object.entries(doStub)}`);

// Create a new `Durable` instance with the durable object's storage
return new this(id, doStub.storage) as InstanceType<T>;
}
}

export class __GlobalDurableObject extends DurableObject {
storage: DurableObjectStorage;

constructor(ctx: DurableObjectCtx, env: DurableObjectEnv) {
super(ctx, env);

throw Error(`${ctx}\n${env}\n${ctx.storage}`);

this.storage = ctx.storage;
}

foo() {
}
}
2 changes: 1 addition & 1 deletion src/runtime/mod.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from "./context.ts";
export * from "./error.ts";
export * from "./runtime.ts";
export * from "./trace.ts";
export * from "./error.ts";
1 change: 1 addition & 0 deletions src/runtime/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export function serverHandler<RegistryT, RegistryCamelT>(
// Error response
const output = {
message: e.message,
stack: e.stack,
};

return new Response(JSON.stringify(output), {
Expand Down

0 comments on commit e4cabfa

Please sign in to comment.