Skip to content
Draft
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
10 changes: 10 additions & 0 deletions examples/cloudflare-durable-object/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "cloudflare-durable-object",
"version": "0.0.0",
"dependencies": {
"sst": "^4"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20240405.0"
}
}
40 changes: 40 additions & 0 deletions examples/cloudflare-durable-object/sst.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/// <reference path="./.sst/platform/config.d.ts" />

/**
* ## Cloudflare Durable Object
*
* This example creates a Durable Object and links it to a worker.
*
* Send a `GET` request to the `url` output. The worker calls the Durable
* Object, and the Durable Object logs the current count.
*/
export default $config({
app(input) {
return {
name: "cloudflare-durable-object",
home: "cloudflare",
removal: input?.stage === "production" ? "retain" : "remove",
};
},
async run() {
const counter = new sst.cloudflare.DurableObject("Counter", {
className: "CounterTest",
});

const api = new sst.cloudflare.Worker("Api", {
durableObjectMigrations: [
{
tag: "v1",
newSqliteClasses: ["CounterTest"],
},
],
handler: "worker.ts",
link: [counter],
url: true,
});

return {
url: api.url,
};
},
});
6 changes: 6 additions & 0 deletions examples/cloudflare-durable-object/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"types": ["@cloudflare/workers-types"]
}
}
28 changes: 28 additions & 0 deletions examples/cloudflare-durable-object/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Resource } from "sst";
import { DurableObject } from "cloudflare:workers";

export default {
async fetch(request: Request) {
const url = new URL(request.url);

if (url.pathname === "/favicon.ico") {
return new Response(null, { status: 204 });
}

const stub = Resource.Counter.getByName("global");
return stub.fetch("https://counter/");
},
};

export class CounterTest extends DurableObject {
async fetch() {
const current = (await this.ctx.storage.get<number>("count")) ?? 0;
const count = current + 1;

await this.ctx.storage.put("count", count);

console.log("durable object hit", { count });

return Response.json({ count });
}
}
32 changes: 32 additions & 0 deletions pkg/types/typescript/typescript_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,38 @@ func TestGenerate(t *testing.T) {
_, err = os.Stat(filepath.Join(ignored, "sst-env.d.ts"))
assert.True(t, os.IsNotExist(err))
})

t.Run("cloudflare durable object binding types", func(t *testing.T) {
dir := setupProject(t, map[string]string{})

pkg, _ := json.Marshal(map[string]interface{}{
"devDependencies": map[string]string{
"@cloudflare/workers-types": "^4.0.0",
},
})
os.WriteFile(filepath.Join(dir, "package.json"), pkg, 0644)

links := common.Links{
"Counter": {
Include: []common.LinkInclude{{
Type: "cloudflare.binding",
Other: map[string]interface{}{
"binding": "durableObjectNamespaceBindings",
},
}},
},
}

err := typescript.Generate(dir, links)
require.NoError(t, err)

content, err := os.ReadFile(filepath.Join(dir, "sst-env.d.ts"))
require.NoError(t, err)

out := string(content)
assert.Contains(t, out, `import * as cloudflare from "@cloudflare/workers-types";`)
assert.Contains(t, out, `"Counter": cloudflare.DurableObjectNamespace`)
})
}

func indexOf(s, substr string) int {
Expand Down
9 changes: 9 additions & 0 deletions platform/src/components/cloudflare/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ export interface HyperdriveBinding {
};
}

export interface DurableObjectNamespaceBinding {
type: "durableObjectNamespaceBindings";
properties: {
className: Input<string>;
scriptName?: Input<string>;
environment?: Input<string>;
};
}

export interface VersionMetadataBinding {
type: "versionMetadataBindings";
properties: Record<string, never>;
Expand Down
104 changes: 104 additions & 0 deletions platform/src/components/cloudflare/durable-object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { ComponentResourceOptions, output } from "@pulumi/pulumi";
import { Component } from "../component.js";
import type { Input } from "../input.js";
import { Link } from "../link.js";
import { binding } from "./binding.js";

export interface DurableObjectArgs {
/**
* The exported Durable Object class name.
*/
className: Input<string>;
}

/**
* Use the `DurableObject` component to register a
* [Cloudflare Durable Object](https://developers.cloudflare.com/durable-objects/)
* for a worker.
*
* Create the Durable Object and then link it to a `sst.cloudflare.Worker`. SST
* adds the Durable Object binding automatically. Configure migrations on the
* worker, similar to Wrangler.
*
* @example
*
* ```ts title="sst.config.ts"
* const counter = new sst.cloudflare.DurableObject("Counter", {
* className: "Counter",
* });
*
* new sst.cloudflare.Worker("Api", {
* handler: "src/worker.ts",
* link: [counter],
* durableObjectMigrations: [{
* tag: "v1",
* newSqliteClasses: ["Counter"],
* }],
* url: true,
* });
* ```
*
* ```ts title="src/worker.ts"
* import { Resource } from "sst";
* import { DurableObject } from "cloudflare:workers";
*
* export default {
* async fetch() {
* const stub = Resource.Counter.getByName("global");
* return stub.fetch("https://counter/");
* },
* };
*
* export class Counter extends DurableObject {
* async fetch() {
* return new Response("hello from the durable object");
* }
* }
* ```
*/
export class DurableObject extends Component implements Link.Linkable {
constructor(
name: string,
private args: DurableObjectArgs,
opts?: ComponentResourceOptions,
) {
super(__pulumiType, name, args, opts);
}

/**
* When you link a Durable Object to a worker, SST adds a Cloudflare Durable
* Object namespace binding.
*
* @internal
*/
getSSTLink() {
return {
properties: {
className: this.args.className,
},
include: [
binding({
type: "durableObjectNamespaceBindings",
properties: {
className: this.args.className,
},
}),
{
type: "cloudflare.durableObject",
className: this.args.className,
},
],
};
}

/**
* The exported Durable Object class name.
*/
public get className() {
return output(this.args.className);
}
}

const __pulumiType = "sst:cloudflare:DurableObject";
// @ts-expect-error
DurableObject.__pulumiType = __pulumiType;
2 changes: 2 additions & 0 deletions platform/src/components/cloudflare/providers/worker-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ interface Inputs {
accountId: string;
scriptName: string;
enabled: boolean;
etag?: string;
}

interface Outputs {
Expand All @@ -15,6 +16,7 @@ export interface WorkerUrlInputs {
accountId: Input<Inputs["accountId"]>;
scriptName: Input<Inputs["scriptName"]>;
enabled: Input<Inputs["enabled"]>;
etag?: Input<Inputs["etag"]>;
}

export interface WorkerUrl {
Expand Down
Loading
Loading