diff --git a/alchemy/package.json b/alchemy/package.json index c8353be5a..4a6d44dae 100644 --- a/alchemy/package.json +++ b/alchemy/package.json @@ -151,6 +151,7 @@ "@iarna/toml": "^2.2.5", "@smithy/node-config-provider": "^4.0.0", "aws4fetch": "^1.0.20", + "effect": "^3.17.8", "env-paths": "^3.0.0", "esbuild": "^0.25.1", "execa": "^9.6.0", diff --git a/alchemy/src/apply.ts b/alchemy/src/apply.ts index 1a3be09a1..a7754dd98 100644 --- a/alchemy/src/apply.ts +++ b/alchemy/src/apply.ts @@ -19,20 +19,6 @@ import { formatFQN } from "./util/cli.ts"; import { logger } from "./util/logger.ts"; import type { Telemetry } from "./util/telemetry/index.ts"; -export interface ApplyOptions { - quiet?: boolean; - alwaysUpdate?: boolean; - noop?: boolean; -} - -export function apply( - resource: PendingResource, - props: ResourceProps | undefined, - options?: ApplyOptions, -): Promise> { - return _apply(resource, props, options); -} - export function isReplacedSignal(error: any): error is ReplacedSignal { return error instanceof Error && (error as any).kind === "ReplacedSignal"; } @@ -47,7 +33,13 @@ export class ReplacedSignal extends Error { } } -async function _apply( +export interface ApplyOptions { + quiet?: boolean; + alwaysUpdate?: boolean; + noop?: boolean; +} + +export async function apply( resource: PendingResource, props: ResourceProps | undefined, options?: ApplyOptions, diff --git a/alchemy/src/cloudflare/bucket.ts b/alchemy/src/cloudflare/bucket.ts index 714b30bc1..2f8b3494c 100644 --- a/alchemy/src/cloudflare/bucket.ts +++ b/alchemy/src/cloudflare/bucket.ts @@ -1,6 +1,7 @@ import { isDeepStrictEqual } from "node:util"; import type { Context } from "../context.ts"; import { Resource, ResourceKind } from "../resource.ts"; +import type { Rune } from "../rune.ts"; import { Scope } from "../scope.ts"; import { withExponentialBackoff } from "../util/retry.ts"; import { CloudflareApiError } from "./api-error.ts"; @@ -334,17 +335,14 @@ export function isBucket(resource: Resource): resource is R2Bucket { * * @see https://developers.cloudflare.com/r2/buckets/ */ -export async function R2Bucket( - id: string, - props: BucketProps = {}, -): Promise { - return await _R2Bucket(id, { +export function R2Bucket(id: string, props: BucketProps = {}) { + return _R2Bucket(id, { ...props, dev: { ...(props.dev ?? {}), force: Scope.current.local, }, - }); + }) as Rune.of; } const _R2Bucket = Resource( diff --git a/alchemy/src/cloudflare/hyperdrive.ts b/alchemy/src/cloudflare/hyperdrive.ts index dc5d681fc..da1d108d4 100644 --- a/alchemy/src/cloudflare/hyperdrive.ts +++ b/alchemy/src/cloudflare/hyperdrive.ts @@ -192,44 +192,43 @@ export interface HyperdriveProps extends CloudflareApiOptions { */ export type Hyperdrive = Resource<"cloudflare::Hyperdrive"> & Omit & { - /** - * The ID of the resource - */ - id: string; - - /** - * Name of the Hyperdrive configuration - */ - name: string; - - /** - * The Cloudflare-generated UUID of the hyperdrive - */ - hyperdriveId: string; + /** + * The ID of the resource + */ + id: string; - /** - * Database connection origin configuration - */ - origin: HyperdrivePublicOrigin | HyperdriveOriginWithAccess; + /** + * Name of the Hyperdrive configuration + */ + name: string; - /** - * Local development configuration - * @internal - */ - dev: { - /** - * The connection string to use for local development - */ - origin: Secret; - }; + /** + * The Cloudflare-generated UUID of the hyperdrive + */ + hyperdriveId: string; + /** + * Database connection origin configuration + */ + origin: HyperdrivePublicOrigin | HyperdriveOriginWithAccess; + /** + * Local development configuration + * @internal + */ + dev: { /** - * Resource type identifier for binding. - * @internal + * The connection string to use for local development */ - type: "hyperdrive"; + origin: Secret; }; + /** + * Resource type identifier for binding. + * @internal + */ + type: "hyperdrive"; +} + /** * Represents a Cloudflare Hyperdrive configuration. * @@ -355,6 +354,9 @@ const _Hyperdrive = Resource( const name = props.name ?? this.output?.name ?? this.scope.createPhysicalName(id); + const name = + props.name ?? this.output?.name ?? this.scope.createPhysicalName(id); + if (this.scope.local) { return this({ id, diff --git a/alchemy/src/cloudflare/queue.ts b/alchemy/src/cloudflare/queue.ts index 7c5770e82..ef82ebe4c 100644 --- a/alchemy/src/cloudflare/queue.ts +++ b/alchemy/src/cloudflare/queue.ts @@ -1,5 +1,6 @@ import type { Context } from "../context.ts"; import { Resource, ResourceKind } from "../resource.ts"; +import type { Rune } from "../rune.ts"; import { Scope } from "../scope.ts"; import { CloudflareApiError, handleApiError } from "./api-error.ts"; import { @@ -230,17 +231,14 @@ export type Queue = Resource<"cloudflare::Queue"> & * * @see https://developers.cloudflare.com/queues/ */ -export async function Queue( - id: string, - props: QueueProps = {}, -): Promise> { - return await _Queue(id, { +export function Queue(id: string, props: QueueProps = {}) { + return _Queue(id, { ...props, dev: { ...(props.dev ?? {}), force: Scope.current.local, }, - }); + }) as Rune.of>; } const _Queue = Resource("cloudflare::Queue", async function < diff --git a/alchemy/src/cloudflare/secret.ts b/alchemy/src/cloudflare/secret.ts index 168dcf98c..4a8ee7610 100644 --- a/alchemy/src/cloudflare/secret.ts +++ b/alchemy/src/cloudflare/secret.ts @@ -1,5 +1,6 @@ import type { Context } from "../context.ts"; import { Resource, ResourceKind } from "../resource.ts"; +import type { Rune } from "../rune.ts"; import { secret as alchemySecret, type Secret as AlchemySecret, @@ -138,19 +139,15 @@ export type Secret = Resource<"cloudflare::Secret"> & * delete: false * }); */ -export async function Secret( - name: string, - props: SecretProps, -): Promise { - // Convert string value to AlchemySecret if needed to prevent plain text serialization - const secretValue = - typeof props.value === "string" ? alchemySecret(props.value) : props.value; - +export function Secret(name: string, props: SecretProps) { // Call the internal resource with secure props return _Secret(name, { ...props, - value: secretValue, - }); + value: + typeof props.value === "string" + ? alchemySecret(props.value) + : props.value, + }) as Rune.of; } const _Secret = Resource( diff --git a/alchemy/src/cloudflare/tunnel.ts b/alchemy/src/cloudflare/tunnel.ts index 564e22eb4..99d2d161f 100644 --- a/alchemy/src/cloudflare/tunnel.ts +++ b/alchemy/src/cloudflare/tunnel.ts @@ -497,7 +497,6 @@ export const Tunnel = Resource( props.name ?? this.output?.name ?? this.scope.createPhysicalName(id); if (this.phase === "update" && this.output.name !== name) { - console.log("replacing tunnel", this.output.name, name); this.replace(true); } diff --git a/alchemy/src/cloudflare/worker.ts b/alchemy/src/cloudflare/worker.ts index 3b1ea4996..bf325e0f2 100644 --- a/alchemy/src/cloudflare/worker.ts +++ b/alchemy/src/cloudflare/worker.ts @@ -58,6 +58,7 @@ import { Workflow, isWorkflow, upsertWorkflow } from "./workflow.ts"; // Previous versions of `Worker` used the `Bundle` resource. // This import is here to avoid errors when destroying the `Bundle` resource. import "../esbuild/bundle.ts"; +import type { Rune } from "../rune.ts"; /** * Configuration options for static assets @@ -677,28 +678,44 @@ export type Worker< * * @example * // Create a worker version for testing with a preview URL: - * const previewWorker = await Worker("my-worker", { + * const previewWorker = Worker("my-worker", { * name: "my-worker", * entrypoint: "./src/worker.ts", * version: "pr-123" * }); * * // The worker will have a preview URL for testing: - * console.log(`Preview URL: ${previewWorker.url}`); + * console.log(`Preview URL: ${await previewWorker.url}`); * // Output: Preview URL: https://pr-123-my-worker.subdomain.workers.dev */ -export function Worker< - const B extends Bindings, - RPC extends Rpc.WorkerEntrypointBranded, ->(id: string, props: WorkerProps): Promise>; - -export function Worker( +export function Worker>( id: string, - props: WorkerProps, -): Promise> { - return _Worker(id, props as WorkerProps); + props: Props, +) { + return _Worker(id, props) as Rune.of< + Worker, awaitRpc> + > & { + // TODO(sam): we have Env and _Env - consolidate + _Env: Worker.Env, awaitRpc>>; + }; } +export declare namespace Worker { + export type Env = Bindings.Runtime< + Rune.await + >; +} + +type awaitRpc> = + Rune.await["rpc"] extends type + ? U & Rpc.WorkerEntrypointBranded + : never; + +type awaitBindings> = Extract< + Rune.await["bindings"], + Bindings | undefined +>; + const _Worker = Resource( "cloudflare::Worker", { @@ -731,7 +748,6 @@ const _Worker = Resource( logger.warn("projectRoot is deprecated, use cwd instead"); props.cwd = props.projectRoot; } - const cwd = path.resolve(props.cwd ?? process.cwd()); const compatibilityDate = props.compatibilityDate ?? DEFAULT_COMPATIBILITY_DATE; diff --git a/alchemy/src/context.ts b/alchemy/src/context.ts index 530248ee4..b5cc85afe 100644 --- a/alchemy/src/context.ts +++ b/alchemy/src/context.ts @@ -11,10 +11,10 @@ import { import type { Scope } from "./scope.ts"; import type { State } from "./state.ts"; -export type Context< - Out extends Resource, - Props extends ResourceProps = ResourceProps, -> = CreateContext | UpdateContext | DeleteContext; +export type Context = + | CreateContext + | UpdateContext + | DeleteContext; export interface CreateContext extends BaseContext { phase: "create"; diff --git a/alchemy/src/index.ts b/alchemy/src/index.ts index 8f39fcc71..221d8df78 100644 --- a/alchemy/src/index.ts +++ b/alchemy/src/index.ts @@ -2,6 +2,7 @@ export type { AlchemyOptions, Phase } from "./alchemy.ts"; export type * from "./context.ts"; export * from "./resource.ts"; +export * from "./rune.ts"; export * from "./scope.ts"; export * from "./secret.ts"; export * from "./serde.ts"; diff --git a/alchemy/src/planetscale/branch.ts b/alchemy/src/planetscale/branch.ts index 8fe7362df..4e31f66ff 100644 --- a/alchemy/src/planetscale/branch.ts +++ b/alchemy/src/planetscale/branch.ts @@ -164,7 +164,6 @@ export const Branch = Resource( // TODO(sam): maybe we don't need to replace? just branch again? or rename? this.replace(); } - if (this.phase === "delete") { if (this.output?.name) { const response = await api.organizations.databases.branches.delete({ @@ -194,6 +193,7 @@ export const Branch = Resource( ? props.parentBranch : props.parentBranch.name; + if (typeof props.parentBranch !== "string" && props.parentBranch) { await waitForDatabaseReady( api, @@ -228,8 +228,8 @@ export const Branch = Resource( ); } - const data = getResponse.data; - const currentParentBranch = data.parent_branch || "main"; +const data = getResponse.data; +const currentParentBranch = data.parent_branch || "main"; // Check immutable properties if (props.parentBranch && parentBranchName !== currentParentBranch) { @@ -262,6 +262,7 @@ export const Branch = Resource( }); } + if (props.clusterSize && data.cluster_name !== props.clusterSize) { await api.organizations.databases.branches.cluster.patch({ path: { diff --git a/alchemy/src/planetscale/database.ts b/alchemy/src/planetscale/database.ts index d90581e1f..427d916fd 100644 --- a/alchemy/src/planetscale/database.ts +++ b/alchemy/src/planetscale/database.ts @@ -192,7 +192,6 @@ export const Database = Resource( body: { new_name: databaseName }, }); } - if (this.phase === "delete") { if (this.output?.name) { const response = await api.organizations.databases.delete({ @@ -212,6 +211,7 @@ export const Database = Resource( return this.destroy(); } + // Check if database exists const getResponse = await api.organizations.databases.get({ path: { diff --git a/alchemy/src/resource.ts b/alchemy/src/resource.ts index 033e78473..9bad10586 100644 --- a/alchemy/src/resource.ts +++ b/alchemy/src/resource.ts @@ -1,6 +1,10 @@ +import * as Effect from "effect/Effect"; +import type * as Schema from "effect/Schema"; +import type { YieldWrap } from "effect/Utils"; import { apply } from "./apply.ts"; import type { Context } from "./context.ts"; import { DestroyStrategy } from "./destroy.ts"; +import { Rune } from "./rune.ts"; import { Scope as _Scope, type Scope } from "./scope.ts"; declare global { @@ -118,15 +122,44 @@ type ResourceLifecycleHandler = ( props: any, ) => Promise>; -// see: https://x.com/samgoodwin89/status/1904640134097887653 -type Handler any> = - | F - | (((this: any, id: string, props?: {}) => never) & IsClass); +type Handler any> = ( + id: string, + props: Resource.input[1]>, +) => Rune.of>>; export function Resource< - const Type extends string, - F extends ResourceLifecycleHandler, ->(type: Type, fn: F): Handler; + const Type extends ResourceKind, + Input extends Schema.Struct.Fields, + Output extends Schema.Struct.Fields, +>( + type: Type, + props: { + input: Input; + output: Output; + }, +): ( + fn: ( + this: Context, Schema.Struct.Type>, + id: string, + props: Schema.Struct.Type, + ) => Generator< + YieldWrap>, + Schema.Struct.Type, + any + >, +) => { + input: Schema.Struct; + output: Schema.Struct.Type; + ( + id: string, + props: Rune.of>, + ): Rune>; +}; + +export function Resource( + type: string, + fn: F, +): Handler; export function Resource< const Type extends string, @@ -134,9 +167,12 @@ export function Resource< >(type: Type, options: Partial, fn: F): Handler; export function Resource< - const Type extends ResourceKind, + const Type extends string, F extends ResourceLifecycleHandler, ->(type: Type, ...args: [Partial, F] | [F]): Handler { +>( + type: Type, + ...args: [options: Partial, handler: F] | [handler: F] +): any { const [options, handler] = args.length === 2 ? args : [undefined, args[0]]; if (PROVIDERS.has(type)) { // We want Alchemy to work in a PNPM monorepo environment unfortunately, @@ -154,53 +190,75 @@ export function Resource< type Out = Awaited>; - const provider = (async ( + const provider = ( resourceID: string, props: ResourceProps, - ): Promise> => { + ): Rune.of> => { const scope = _Scope.current; - if (resourceID.includes(":")) { // we want to use : as an internal separator for resources throw new Error(`ID cannot include colons: ${resourceID}`); } + return Rune( + Effect.promise(() => { + if (scope.resources.has(resourceID)) { + // TODO(sam): do we want to throw? + // it's kind of awesome that you can re-create a resource and call apply + const otherResource = scope.resources.get(resourceID); + if (otherResource?.[ResourceKind] !== type) { + scope.fail(); + const error = new Error( + `Resource ${resourceID} already exists in the stack and is of a different type: '${otherResource?.[ResourceKind]}' !== '${type}'`, + ); + scope.telemetryClient.record({ + event: "resource.error", + resource: type, + error, + }); + throw error; + } + } - if (scope.resources.has(resourceID)) { - // TODO(sam): do we want to throw? - // it's kind of awesome that you can re-create a resource and call apply - const otherResource = scope.resources.get(resourceID); - if (otherResource?.[ResourceKind] !== type) { - scope.fail(); - const error = new Error( - `Resource ${resourceID} already exists in the stack and is of a different type: '${otherResource?.[ResourceKind]}' !== '${type}'`, - ); - scope.telemetryClient.record({ - event: "resource.error", - resource: type, - error, - }); - throw error; - } - } - - // get a sequence number (unique within the scope) for the resource - const seq = scope.seq(); - const meta = { - [ResourceKind]: type, - [ResourceID]: resourceID, - [ResourceFQN]: scope.fqn(resourceID), - [ResourceSeq]: seq, - [ResourceScope]: scope, - [DestroyStrategy]: options?.destroyStrategy ?? "sequential", - } as any as PendingResource; - const promise = apply(meta, props, options); - const resource = Object.assign(promise, meta); - scope.resources.set(resourceID, resource); - return resource; - }) as Provider; + // get a sequence number (unique within the scope) for the resource + const seq = scope.seq(); + const meta = { + [ResourceKind]: type, + [ResourceID]: resourceID, + [ResourceFQN]: scope.fqn(resourceID), + [ResourceSeq]: seq, + [ResourceScope]: scope, + [DestroyStrategy]: options?.destroyStrategy ?? "sequential", + } as any as PendingResource; + const promise = apply(meta, props, options); + const resource = Object.assign(promise, meta); + scope.resources.set(resourceID, resource); + return resource; + }), + ) as Rune.of>; + }; provider.type = type; provider.handler = handler; provider.options = options; PROVIDERS.set(type, provider); return provider; } + +export declare namespace Resource { + export type input = + | T + | Rune.of + | (T extends any[] + ? array + : { + [k in keyof T]: input; + }); + + type array< + T extends any[], + Accum extends any[] = [], + > = number extends T["length"] + ? input[] + : T extends [infer Head, ...infer Tail] + ? array]> + : Accum; +} diff --git a/alchemy/src/rune.ts b/alchemy/src/rune.ts new file mode 100644 index 000000000..4cd41ace7 --- /dev/null +++ b/alchemy/src/rune.ts @@ -0,0 +1,114 @@ +import * as Effect from "effect/Effect"; +import type { Binding } from "./cloudflare/bindings.ts"; +import type { Resource } from "./resource.ts"; +import type { type } from "./type.ts"; + +export interface Rune extends PromiseLike, Effect.Effect {} + +export function Rune(effect: Effect.Effect): Rune.of { + let cache: Promise | undefined; + const rune = Effect.promise(() => (cache ??= Effect.runPromise(effect))); + return new Proxy(() => {}, { + apply: (_, _this, args) => + Rune(Effect.map(rune, (value: any) => value(...args))), + get(_: any, prop: any) { + if (prop in rune) { + return bind(rune, prop); + } else if (["then", "catch", "finally"].includes(prop as any)) { + return (...args: any[]) => + (Effect.runPromise(rune) as any)[prop](...args); + } else { + return Rune(rune.pipe(Effect.map((x) => bind(x, prop)))); + } + function bind(self: any, prop: any) { + const member = self[prop]; + return typeof member === "function" ? member.bind(self) : member; + } + }, + }) as Rune.of; +} + +export declare namespace Rune { + export type of = T extends (...args: infer Args) => infer U + ? (...args: array) => of> + : T extends any[] + ? array + : T extends object + ? Rune & { + [k in keyof T]: of>; + } + : Rune; + + type array = number extends T["length"] + ? Rune.of[] + : T extends [infer Head, ...infer Tail] + ? [Head | Rune.of, ...array] + : []; + + export type await = T extends type + ? T + : T extends Resource + ? T + : T extends Rune + ? U + : T extends Effect.Effect + ? T + : T extends Binding + ? T + : T extends PromiseLike | Effect.Effect + ? await + : T extends any[] + ? awaitArray + : T extends object + ? { + [k in keyof T]: await; + } + : T; + + type awaitArray = number extends T["length"] + ? await[] + : T extends [infer Head, ...infer Tail] + ? [await, ...awaitArray] + : []; +} + +const myEffect = Effect.gen(function* () { + console.log("Hello via Effect!"); + yield* Effect.sleep(1); + return { + nested: { + number: Math.floor(Math.random() * 100), + fn: () => 1, + }, + }; +}); +const myRune = Rune(myEffect); + +const res1 = await myRune; +const val1 = await myRune.nested.number; +const val2 = await myRune.pipe( + Effect.map((e) => e.nested.number), + Effect.runPromise, +); + +class Counter { + count = 0; + increment() { + return this.count++; + } +} +const g = Rune(Effect.succeed(new Counter())); + +console.log({ + res1, + val1, + val2, + fn: await myRune.nested.fn(), + inc: await g.increment(), +}); + +// const counter = new Counter(); +// const counterRune = Rune(Effect.gen(function* () { +// yield* Effect.sleep(1); +// counter.increment(); +// })); diff --git a/alchemy/src/scope.ts b/alchemy/src/scope.ts index 60be1c404..1ee60de00 100644 --- a/alchemy/src/scope.ts +++ b/alchemy/src/scope.ts @@ -293,6 +293,8 @@ export class Scope { return state !== undefined && (type === undefined || state.kind === type); } + async fetch(request: Request) {} + public createPhysicalName(id: string, delimiter = "-"): string { const app = this.appName; const stage = this.stage; diff --git a/bun.lock b/bun.lock index 586797a66..b0fbbbba2 100644 --- a/bun.lock +++ b/bun.lock @@ -35,6 +35,7 @@ "@iarna/toml": "^2.2.5", "@smithy/node-config-provider": "^4.0.0", "aws4fetch": "^1.0.20", + "effect": "^3.17.8", "env-paths": "^3.0.0", "esbuild": "^0.25.1", "execa": "^9.6.0", @@ -2916,7 +2917,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "effect": ["effect@3.15.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-0JNwvfs4Wwbo3f6IOydBFlp+zxuO8Iny2UAWNW3+FNn9x8FJf7q67QnQagUZgPl/BLl/xuPLVksrmNyIrJ8k/Q=="], + "effect": ["effect@3.17.8", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-3X2DahqmaTwDdvdYuX/MFhYA4srjO21NodMWhCXPMRK/3IQlByJyNFpZrXCWfnMrlr6DsLI+EgI3rqqAQtWrIA=="], "electron-to-chromium": ["electron-to-chromium@1.5.209", "", {}, "sha512-Xoz0uMrim9ZETCQt8UgM5FxQF9+imA7PBpokoGcZloA1uw2LeHzTlip5cb5KOAsXZLjh/moN2vReN3ZjJmjI9A=="], @@ -5098,16 +5099,8 @@ "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], - "@effect/cluster/effect": ["effect@3.16.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg=="], - "@effect/platform-node/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], - "@effect/platform-node-shared/effect": ["effect@3.16.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg=="], - - "@effect/rpc/effect": ["effect@3.16.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg=="], - - "@effect/sql/effect": ["effect@3.16.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg=="], - "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -5138,8 +5131,12 @@ "@livestore/devtools-vite/vite": ["vite@7.1.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw=="], + "@livestore/peer-deps/effect": ["effect@3.15.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-0JNwvfs4Wwbo3f6IOydBFlp+zxuO8Iny2UAWNW3+FNn9x8FJf7q67QnQagUZgPl/BLl/xuPLVksrmNyIrJ8k/Q=="], + "@livestore/react/react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="], + "@livestore/utils/effect": ["effect@3.15.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-0JNwvfs4Wwbo3f6IOydBFlp+zxuO8Iny2UAWNW3+FNn9x8FJf7q67QnQagUZgPl/BLl/xuPLVksrmNyIrJ8k/Q=="], + "@livestore/utils/nanoid": ["nanoid@5.1.3", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-zAbEOEr7u2CbxwoMRlz/pNSpRP0FdAU4pRaYunCdEezWohXFs+a0Xw7RfkKaezMsmSM1vttcLthJtwRnVtOfHQ=="], "@netlify/dev-utils/find-up": ["find-up@7.0.0", "", { "dependencies": { "locate-path": "^7.2.0", "path-exists": "^5.0.0", "unicorn-magic": "^0.1.0" } }, "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g=="], diff --git a/examples/cloudflare-worker/alchemy.run.ts b/examples/cloudflare-worker/alchemy.run.ts index 0af7e16d9..57c2133d4 100644 --- a/examples/cloudflare-worker/alchemy.run.ts +++ b/examples/cloudflare-worker/alchemy.run.ts @@ -1,4 +1,4 @@ -import alchemy, { type } from "alchemy"; +import alchemy, { Resource, type } from "alchemy"; import { DurableObjectNamespace, Queue, @@ -6,34 +6,36 @@ import { Worker, Workflow, } from "alchemy/cloudflare"; +import * as Console from "effect/Console"; +import * as Schema from "effect/Schema"; import type { HelloWorldDO } from "./src/do.ts"; import type MyRPC from "./src/rpc.ts"; -const app = await alchemy("cloudflare-worker"); +export default alchemy("cloudflare-worker"); -export const queue = await Queue<{ +export const queue = Queue<{ name: string; email: string; }>("queue", { - name: `${app.name}-${app.stage}-queue`, adopt: true, }); -export const rpc = await Worker("rpc", { - name: `${app.name}-${app.stage}-rpc`, +export const rpc = Worker("rpc", { entrypoint: "./src/rpc.ts", rpc: type, adopt: true, }); -export const worker = await Worker("worker", { - name: `${app.name}-${app.stage}-worker`, +const bucket = R2Bucket("bucket", { + adopt: true, +}); + +export const worker = Worker("worker", { entrypoint: "./src/worker.ts", bindings: { - BUCKET: await R2Bucket("bucket", { - name: `${app.name}-${app.stage}-bucket`, - adopt: true, - }), + bucketID: bucket.name, + RPC: rpc, + BUCKET: bucket, QUEUE: queue, WORKFLOW: Workflow("OFACWorkflow", { className: "OFACWorkflow", @@ -43,7 +45,6 @@ export const worker = await Worker("worker", { className: "HelloWorldDO", sqlite: true, }), - RPC: rpc, }, url: true, eventSources: [queue], @@ -55,6 +56,29 @@ export const worker = await Worker("worker", { adopt: true, }); -console.log(worker.url); -await app.finalize(); +console.log({ + url: await worker.url, +}); + +export type MyEffectResource = typeof MyEffectResource.output; + +export const MyEffectResource = Resource("my-effect-resource", { + input: { + key: Schema.String, + }, + output: { + /** + * The value of the resource. + */ + value: Schema.Number, + }, +})(function* (id, props) { + // ... + + yield* Console.log(id); + + return { + value: 1, + }; +}); diff --git a/examples/cloudflare-worker/src/env.ts b/examples/cloudflare-worker/src/env.ts index 0350f8fe8..11b56e4fd 100644 --- a/examples/cloudflare-worker/src/env.ts +++ b/examples/cloudflare-worker/src/env.ts @@ -1,7 +1,7 @@ import type { worker } from "../alchemy.run.js"; declare global { - export type CloudflareEnv = typeof worker.Env; + export type CloudflareEnv = typeof worker._Env; } declare module "cloudflare:workers" { diff --git a/examples/cloudflare-worker/src/rpc.ts b/examples/cloudflare-worker/src/rpc.ts index 31ca32f5c..fcebdec33 100644 --- a/examples/cloudflare-worker/src/rpc.ts +++ b/examples/cloudflare-worker/src/rpc.ts @@ -7,6 +7,9 @@ export default class MyRPC extends WorkerEntrypoint { async hello(name: string) { return `Hello, ${name}!`; } + async generic(value: T): Promise { + return value; + } async fetch() { return new Response("Hello from Worker B"); } diff --git a/examples/cloudflare-worker/src/worker.ts b/examples/cloudflare-worker/src/worker.ts index 7196891da..8ffc112e6 100644 --- a/examples/cloudflare-worker/src/worker.ts +++ b/examples/cloudflare-worker/src/worker.ts @@ -11,6 +11,7 @@ export default { }); const obj = env.DO.get(env.DO.idFromName("foo")); + await obj.increment(); async function _foo() { // @ts-expect-error - foo doesn't exist on the HelloWorldDO class diff --git a/tsconfig.base.json b/tsconfig.base.json index 502882bb8..220b7216b 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -27,6 +27,6 @@ "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false, - "noImplicitThis": true + "noImplicitThis": true, } }