alchemy 2.0.0-beta.23 → 2.0.0-beta.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/alchemy.js +12589 -2451
- package/bin/alchemy.js.map +1 -1
- package/bin/alchemy.ts +2 -0
- package/bin/cli.js +18 -0
- package/bin/commands/bootstrap.ts +88 -4
- package/bin/commands/deploy.ts +54 -20
- package/bin/commands/dev.ts +45 -0
- package/bin/exec.ts +22 -0
- package/lib/AdoptPolicy.d.ts +2 -2
- package/lib/AdoptPolicy.d.ts.map +1 -1
- package/lib/AdoptPolicy.js.map +1 -1
- package/lib/AlchemyContext.d.ts +1 -0
- package/lib/AlchemyContext.d.ts.map +1 -1
- package/lib/AlchemyContext.js +1 -0
- package/lib/AlchemyContext.js.map +1 -1
- package/lib/Build/Command.d.ts.map +1 -1
- package/lib/Build/Command.js +8 -1
- package/lib/Build/Command.js.map +1 -1
- package/lib/Bundle/Bundle.d.ts +21 -9
- package/lib/Bundle/Bundle.d.ts.map +1 -1
- package/lib/Bundle/Bundle.js +29 -15
- package/lib/Bundle/Bundle.js.map +1 -1
- package/lib/Cloudflare/Local/Sidecar.d.ts +27 -0
- package/lib/Cloudflare/Local/Sidecar.d.ts.map +1 -0
- package/lib/Cloudflare/Local/Sidecar.js +24 -0
- package/lib/Cloudflare/Local/Sidecar.js.map +1 -0
- package/lib/Cloudflare/Local/SidecarHandlers.d.ts +5 -0
- package/lib/Cloudflare/Local/SidecarHandlers.d.ts.map +1 -0
- package/lib/Cloudflare/Local/SidecarHandlers.js +119 -0
- package/lib/Cloudflare/Local/SidecarHandlers.js.map +1 -0
- package/lib/Cloudflare/Local/SidecarServer.d.ts +2 -0
- package/lib/Cloudflare/Local/SidecarServer.d.ts.map +1 -0
- package/lib/Cloudflare/Local/SidecarServer.js +27 -0
- package/lib/Cloudflare/Local/SidecarServer.js.map +1 -0
- package/lib/Cloudflare/SecretsStore/Secret.d.ts.map +1 -1
- package/lib/Cloudflare/SecretsStore/Secret.js +55 -7
- package/lib/Cloudflare/SecretsStore/Secret.js.map +1 -1
- package/lib/Cloudflare/SecretsStore/SecretsStore.d.ts.map +1 -1
- package/lib/Cloudflare/SecretsStore/SecretsStore.js +35 -11
- package/lib/Cloudflare/SecretsStore/SecretsStore.js.map +1 -1
- package/lib/Cloudflare/StateStore/State.d.ts +35 -0
- package/lib/Cloudflare/StateStore/State.d.ts.map +1 -1
- package/lib/Cloudflare/StateStore/State.js +187 -45
- package/lib/Cloudflare/StateStore/State.js.map +1 -1
- package/lib/Cloudflare/StateStore/index.d.ts +1 -1
- package/lib/Cloudflare/StateStore/index.d.ts.map +1 -1
- package/lib/Cloudflare/StateStore/index.js +1 -1
- package/lib/Cloudflare/StateStore/index.js.map +1 -1
- package/lib/Cloudflare/Workers/Assets.d.ts +1 -1
- package/lib/Cloudflare/Workers/Assets.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Assets.js +6 -1
- package/lib/Cloudflare/Workers/Assets.js.map +1 -1
- package/lib/Cloudflare/Workers/Compatibility.d.ts +6 -0
- package/lib/Cloudflare/Workers/Compatibility.d.ts.map +1 -0
- package/lib/Cloudflare/Workers/Compatibility.js +10 -0
- package/lib/Cloudflare/Workers/Compatibility.js.map +1 -0
- package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts +7 -0
- package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -0
- package/lib/Cloudflare/Workers/LocalWorkerProvider.js +81 -0
- package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -0
- package/lib/Cloudflare/Workers/Worker.d.ts +3 -1
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js +54 -7
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/lib/Cloudflare/Workers/WorkerBundle.d.ts +31 -0
- package/lib/Cloudflare/Workers/WorkerBundle.d.ts.map +1 -0
- package/lib/Cloudflare/Workers/WorkerBundle.js +174 -0
- package/lib/Cloudflare/Workers/WorkerBundle.js.map +1 -0
- package/lib/Cloudflare/Workers/WorkerName.d.ts +3 -0
- package/lib/Cloudflare/Workers/WorkerName.d.ts.map +1 -0
- package/lib/Cloudflare/Workers/WorkerName.js +9 -0
- package/lib/Cloudflare/Workers/WorkerName.js.map +1 -0
- package/lib/Cloudflare/Workers/index.d.ts +1 -0
- package/lib/Cloudflare/Workers/index.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/index.js +1 -0
- package/lib/Cloudflare/Workers/index.js.map +1 -1
- package/lib/Platform.d.ts +1 -1
- package/lib/Platform.d.ts.map +1 -1
- package/lib/Sidecar/Lock.d.ts +24 -0
- package/lib/Sidecar/Lock.d.ts.map +1 -0
- package/lib/Sidecar/Lock.js +102 -0
- package/lib/Sidecar/Lock.js.map +1 -0
- package/lib/Sidecar/RpcClient.d.ts +15 -0
- package/lib/Sidecar/RpcClient.d.ts.map +1 -0
- package/lib/Sidecar/RpcClient.js +75 -0
- package/lib/Sidecar/RpcClient.js.map +1 -0
- package/lib/Sidecar/RpcHandler.d.ts +48 -0
- package/lib/Sidecar/RpcHandler.d.ts.map +1 -0
- package/lib/Sidecar/RpcHandler.js +64 -0
- package/lib/Sidecar/RpcHandler.js.map +1 -0
- package/lib/Sidecar/RpcPaths.d.ts +14 -0
- package/lib/Sidecar/RpcPaths.d.ts.map +1 -0
- package/lib/Sidecar/RpcPaths.js +22 -0
- package/lib/Sidecar/RpcPaths.js.map +1 -0
- package/lib/Sidecar/RpcServer.d.ts +10 -0
- package/lib/Sidecar/RpcServer.d.ts.map +1 -0
- package/lib/Sidecar/RpcServer.js +50 -0
- package/lib/Sidecar/RpcServer.js.map +1 -0
- package/lib/Sidecar/RpcTransport.d.ts +22 -0
- package/lib/Sidecar/RpcTransport.d.ts.map +1 -0
- package/lib/Sidecar/RpcTransport.js +81 -0
- package/lib/Sidecar/RpcTransport.js.map +1 -0
- package/lib/Telemetry/Metrics.d.ts +15 -0
- package/lib/Telemetry/Metrics.d.ts.map +1 -1
- package/lib/Telemetry/Metrics.js +20 -0
- package/lib/Telemetry/Metrics.js.map +1 -1
- package/lib/Util/PlatformServices.d.ts +3 -0
- package/lib/Util/PlatformServices.d.ts.map +1 -1
- package/lib/Util/PlatformServices.js +13 -0
- package/lib/Util/PlatformServices.js.map +1 -1
- package/lib/Util/layer-scoped.d.ts +16 -0
- package/lib/Util/layer-scoped.d.ts.map +1 -0
- package/lib/Util/layer-scoped.js +14 -0
- package/lib/Util/layer-scoped.js.map +1 -0
- package/package.json +4 -2
- package/src/AdoptPolicy.ts +9 -9
- package/src/AlchemyContext.ts +2 -0
- package/src/Build/Command.ts +8 -1
- package/src/Bundle/Bundle.ts +65 -25
- package/src/Cloudflare/Local/Sidecar.ts +44 -0
- package/src/Cloudflare/Local/SidecarHandlers.ts +182 -0
- package/src/Cloudflare/Local/SidecarServer.ts +57 -0
- package/src/Cloudflare/SecretsStore/Secret.ts +78 -7
- package/src/Cloudflare/SecretsStore/SecretsStore.ts +47 -12
- package/src/Cloudflare/StateStore/State.ts +281 -64
- package/src/Cloudflare/StateStore/index.ts +1 -1
- package/src/Cloudflare/Workers/Assets.ts +10 -1
- package/src/Cloudflare/Workers/Compatibility.ts +12 -0
- package/src/Cloudflare/Workers/LocalWorkerProvider.ts +93 -0
- package/src/Cloudflare/Workers/Worker.ts +102 -19
- package/src/Cloudflare/Workers/WorkerBundle.ts +234 -0
- package/src/Cloudflare/Workers/WorkerName.ts +10 -0
- package/src/Cloudflare/Workers/index.ts +1 -0
- package/src/Platform.ts +1 -1
- package/src/Sidecar/Lock.ts +190 -0
- package/src/Sidecar/RpcClient.ts +141 -0
- package/src/Sidecar/RpcHandler.ts +169 -0
- package/src/Sidecar/RpcPaths.ts +29 -0
- package/src/Sidecar/RpcServer.ts +80 -0
- package/src/Sidecar/RpcTransport.ts +85 -0
- package/src/Telemetry/Metrics.ts +38 -0
- package/src/Util/PlatformServices.ts +19 -0
- package/src/Util/layer-scoped.ts +25 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { newWebSocketRpcSession } from "capnweb";
|
|
2
|
+
import * as Context from "effect/Context";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Fiber from "effect/Fiber";
|
|
5
|
+
import * as FileSystem from "effect/FileSystem";
|
|
6
|
+
import * as Layer from "effect/Layer";
|
|
7
|
+
import * as Schedule from "effect/Schedule";
|
|
8
|
+
import * as Schema from "effect/Schema";
|
|
9
|
+
import * as ChildProcess from "effect/unstable/process/ChildProcess";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import * as Lock from "./Lock.ts";
|
|
12
|
+
import {
|
|
13
|
+
deserializeRpcHandlers,
|
|
14
|
+
type RpcHandlerDecoders,
|
|
15
|
+
type RpcHandlers,
|
|
16
|
+
type SerializedRpcHandlers,
|
|
17
|
+
} from "./RpcHandler.ts";
|
|
18
|
+
import * as RpcPaths from "./RpcPaths.ts";
|
|
19
|
+
|
|
20
|
+
export const RpcClientService = <Self, T extends RpcHandlers>() =>
|
|
21
|
+
Context.Service<Self, T>();
|
|
22
|
+
|
|
23
|
+
class RpcClientError extends Schema.TaggedErrorClass<RpcClientError>()(
|
|
24
|
+
"RpcClientError",
|
|
25
|
+
{
|
|
26
|
+
reason: Schema.Literals(["InvalidURL", "WebSocketError"]),
|
|
27
|
+
message: Schema.String,
|
|
28
|
+
cause: Schema.optional(Schema.DefectWithStack),
|
|
29
|
+
},
|
|
30
|
+
) {}
|
|
31
|
+
|
|
32
|
+
export const layer = <Self, T extends RpcHandlers>(
|
|
33
|
+
tag: Context.ServiceClass<Self, any, T>,
|
|
34
|
+
options: {
|
|
35
|
+
main: string;
|
|
36
|
+
schema: RpcHandlerDecoders<T>;
|
|
37
|
+
},
|
|
38
|
+
) =>
|
|
39
|
+
Layer.effect(
|
|
40
|
+
tag,
|
|
41
|
+
Effect.gen(function* () {
|
|
42
|
+
const fiber = yield* maybeStartRpcServer(
|
|
43
|
+
fileURLToPath(options.main),
|
|
44
|
+
).pipe(
|
|
45
|
+
Effect.flatMap(() => RpcSession),
|
|
46
|
+
Effect.map((session) =>
|
|
47
|
+
deserializeRpcHandlers(
|
|
48
|
+
session as SerializedRpcHandlers<T>,
|
|
49
|
+
options.schema,
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
Effect.retry(Schedule.exponential("100 millis")),
|
|
53
|
+
Effect.provide(
|
|
54
|
+
Layer.provideMerge(Lock.LockLive, RpcPaths.layer(options.main)),
|
|
55
|
+
),
|
|
56
|
+
Effect.forkScoped,
|
|
57
|
+
);
|
|
58
|
+
return new Proxy({} as T, {
|
|
59
|
+
get(target, prop) {
|
|
60
|
+
return (...args: any[]) =>
|
|
61
|
+
Fiber.join(fiber).pipe(
|
|
62
|
+
Effect.flatMap((session) => session[prop as never](...args)),
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const maybeStartRpcServer = Effect.fn(function* (main: string) {
|
|
70
|
+
const lock = yield* Lock.Lock;
|
|
71
|
+
if (!(yield* lock.check)) {
|
|
72
|
+
yield* Effect.logDebug("[RpcClient] Starting RPC server", main);
|
|
73
|
+
yield* ChildProcess.make("bun", ["run", main], {
|
|
74
|
+
stdout: "inherit",
|
|
75
|
+
stderr: "inherit",
|
|
76
|
+
detached: true,
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
yield* Effect.logDebug("[RpcClient] RPC server already running", main);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const RpcSession = Effect.gen(function* () {
|
|
84
|
+
const paths = yield* RpcPaths.RpcPaths;
|
|
85
|
+
const fs = yield* FileSystem.FileSystem;
|
|
86
|
+
const ws = yield* fs.readFileString(paths.url).pipe(
|
|
87
|
+
Effect.flatMap((url) =>
|
|
88
|
+
Effect.try({
|
|
89
|
+
try: () => new URL(url),
|
|
90
|
+
catch: (e) =>
|
|
91
|
+
new RpcClientError({
|
|
92
|
+
reason: "InvalidURL",
|
|
93
|
+
message: `"${url}" is not a valid URL`,
|
|
94
|
+
cause: e,
|
|
95
|
+
}),
|
|
96
|
+
}),
|
|
97
|
+
),
|
|
98
|
+
Effect.flatMap((url) =>
|
|
99
|
+
Effect.callback<WebSocket, RpcClientError>((resume) => {
|
|
100
|
+
const ws = new WebSocket(url);
|
|
101
|
+
ws.onopen = () => resume(Effect.succeed(ws));
|
|
102
|
+
ws.onerror = (e) =>
|
|
103
|
+
resume(
|
|
104
|
+
Effect.fail(
|
|
105
|
+
new RpcClientError({
|
|
106
|
+
reason: "WebSocketError",
|
|
107
|
+
message: "WebSocket connection failed",
|
|
108
|
+
cause: e,
|
|
109
|
+
}),
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
return Effect.sync(() => ws.close());
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
115
|
+
Effect.retry({
|
|
116
|
+
while: (e) => e.reason !== "WebSocketError",
|
|
117
|
+
schedule: Schedule.spaced("50 millis"),
|
|
118
|
+
times: 100,
|
|
119
|
+
}),
|
|
120
|
+
Effect.retry({
|
|
121
|
+
while: (e) => e.reason === "WebSocketError",
|
|
122
|
+
schedule: Schedule.exponential("100 millis"),
|
|
123
|
+
times: 3,
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
const session = yield* Effect.acquireRelease(
|
|
127
|
+
Effect.sync(() =>
|
|
128
|
+
newWebSocketRpcSession<{
|
|
129
|
+
heartbeat: () => Promise<void>;
|
|
130
|
+
shutdown: () => Promise<void>;
|
|
131
|
+
}>(ws),
|
|
132
|
+
),
|
|
133
|
+
(session) => Effect.sync(() => session[Symbol.dispose]()),
|
|
134
|
+
);
|
|
135
|
+
yield* Effect.promise(() => session.heartbeat()).pipe(
|
|
136
|
+
Effect.repeat(Schedule.spaced("1 second")),
|
|
137
|
+
Effect.ensuring(Effect.promise(() => session.shutdown())),
|
|
138
|
+
Effect.forkScoped,
|
|
139
|
+
);
|
|
140
|
+
return session;
|
|
141
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { RpcCompatible } from "capnweb";
|
|
2
|
+
import * as Cause from "effect/Cause";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Exit from "effect/Exit";
|
|
5
|
+
import * as Schema from "effect/Schema";
|
|
6
|
+
|
|
7
|
+
type RpcHandler<Args extends Array<any>, Success, Error> = (
|
|
8
|
+
...args: Args
|
|
9
|
+
) => Effect.Effect<Success, Error>;
|
|
10
|
+
|
|
11
|
+
type SerializedRpcHandler<Args extends Array<any>, Success, Error> = (
|
|
12
|
+
...args: Args
|
|
13
|
+
) => Promise<SerializedExit<RpcCompatible<Success>, RpcCompatible<Error>>>;
|
|
14
|
+
|
|
15
|
+
type SerializedCause<Error> =
|
|
16
|
+
| { _tag: "Fail"; error: Error }
|
|
17
|
+
| { _tag: "Die"; defect: unknown }
|
|
18
|
+
| { _tag: "Interrupt"; fiberId: number | undefined };
|
|
19
|
+
|
|
20
|
+
type SerializedExit<Success, Error> =
|
|
21
|
+
| { _tag: "Success"; value: Success }
|
|
22
|
+
| {
|
|
23
|
+
_tag: "Failure";
|
|
24
|
+
cause: Array<SerializedCause<Error>>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type RpcHandlers = Record<string, RpcHandler<any, any, any>>;
|
|
28
|
+
|
|
29
|
+
export type SerializedRpcHandlers<T extends RpcHandlers> = {
|
|
30
|
+
[K in keyof T]: T[K] extends RpcHandler<
|
|
31
|
+
infer Args,
|
|
32
|
+
infer Success,
|
|
33
|
+
infer Error
|
|
34
|
+
>
|
|
35
|
+
? SerializedRpcHandler<Args, Success, Error>
|
|
36
|
+
: never;
|
|
37
|
+
} extends infer O extends object & RpcCompatible<O>
|
|
38
|
+
? O
|
|
39
|
+
: never;
|
|
40
|
+
|
|
41
|
+
type RpcHandlerEncoder<Success, Error> = {
|
|
42
|
+
success: Schema.Encoder<Success>;
|
|
43
|
+
error: Schema.Encoder<Error>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type RpcHandlerEncoders<T extends RpcHandlers> = {
|
|
47
|
+
[K in keyof T]: T[K] extends RpcHandler<any, infer Success, infer Error>
|
|
48
|
+
? RpcHandlerEncoder<Success, Error>
|
|
49
|
+
: never;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type RpcHandlerDecoder<Success, Error> = {
|
|
53
|
+
success: Schema.Decoder<Success>;
|
|
54
|
+
error: Schema.Decoder<Error>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type RpcHandlerDecoders<T extends RpcHandlers> = {
|
|
58
|
+
[K in keyof T]: T[K] extends RpcHandler<any, infer Success, infer Error>
|
|
59
|
+
? RpcHandlerDecoder<Success, Error>
|
|
60
|
+
: never;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const serializeRpcHandler =
|
|
64
|
+
<Args extends Array<any>, Success, Error>(
|
|
65
|
+
handler: RpcHandler<Args, Success, Error>,
|
|
66
|
+
schema: RpcHandlerEncoder<Success, Error>,
|
|
67
|
+
): SerializedRpcHandler<Args, Success, Error> =>
|
|
68
|
+
(...args) =>
|
|
69
|
+
handler(...args).pipe(
|
|
70
|
+
Effect.exit,
|
|
71
|
+
Effect.map((exit): SerializedExit<Success, Error> => {
|
|
72
|
+
if (exit._tag === "Success") {
|
|
73
|
+
return {
|
|
74
|
+
_tag: "Success",
|
|
75
|
+
value: Schema.encodeSync(schema.success)(exit.value),
|
|
76
|
+
} as const;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
_tag: "Failure",
|
|
80
|
+
cause: exit.cause.reasons.map((reason): SerializedCause<Error> => {
|
|
81
|
+
switch (reason._tag) {
|
|
82
|
+
case "Fail":
|
|
83
|
+
return {
|
|
84
|
+
_tag: "Fail",
|
|
85
|
+
error: Schema.encodeSync(schema.error)(reason.error),
|
|
86
|
+
};
|
|
87
|
+
case "Die":
|
|
88
|
+
return {
|
|
89
|
+
_tag: "Die",
|
|
90
|
+
defect: Schema.encodeSync(Schema.DefectWithStack)(
|
|
91
|
+
reason.defect,
|
|
92
|
+
),
|
|
93
|
+
};
|
|
94
|
+
case "Interrupt":
|
|
95
|
+
return { _tag: "Interrupt", fiberId: reason.fiberId };
|
|
96
|
+
}
|
|
97
|
+
}),
|
|
98
|
+
} as const;
|
|
99
|
+
}),
|
|
100
|
+
Effect.runPromise,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
export const serializeRpcHandlers = <T extends RpcHandlers>(
|
|
104
|
+
handlers: T,
|
|
105
|
+
schema: RpcHandlerEncoders<T>,
|
|
106
|
+
): SerializedRpcHandlers<T> =>
|
|
107
|
+
Object.fromEntries(
|
|
108
|
+
Object.entries(schema).map(([key, schema]) => [
|
|
109
|
+
key,
|
|
110
|
+
serializeRpcHandler(handlers[key], schema),
|
|
111
|
+
]),
|
|
112
|
+
) as SerializedRpcHandlers<T>;
|
|
113
|
+
|
|
114
|
+
const deserializeRpcHandler =
|
|
115
|
+
<Args extends Array<any>, Success, Error>(
|
|
116
|
+
handler: SerializedRpcHandler<Args, Success, Error>,
|
|
117
|
+
schema: RpcHandlerDecoder<Success, Error>,
|
|
118
|
+
): RpcHandler<Args, Success, Error> =>
|
|
119
|
+
(...args) =>
|
|
120
|
+
Effect.promise(async () => {
|
|
121
|
+
try {
|
|
122
|
+
return await handler(...args);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error("Error calling handler", error);
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}).pipe(
|
|
128
|
+
Effect.map((exit): Exit.Exit<Success, Error> => {
|
|
129
|
+
if (exit._tag === "Success") {
|
|
130
|
+
return Exit.succeed(Schema.decodeSync(schema.success)(exit.value));
|
|
131
|
+
}
|
|
132
|
+
return Exit.failCause(
|
|
133
|
+
Cause.fromReasons(
|
|
134
|
+
exit.cause.map((reason): Cause.Reason<Error> => {
|
|
135
|
+
switch (reason._tag) {
|
|
136
|
+
case "Fail":
|
|
137
|
+
return Cause.makeFailReason(
|
|
138
|
+
Schema.decodeSync(schema.error)(reason.error),
|
|
139
|
+
);
|
|
140
|
+
case "Die":
|
|
141
|
+
return Cause.makeDieReason(
|
|
142
|
+
Schema.decodeSync(Schema.DefectWithStack)(reason.defect),
|
|
143
|
+
);
|
|
144
|
+
case "Interrupt":
|
|
145
|
+
return Cause.makeInterruptReason(reason.fiberId);
|
|
146
|
+
}
|
|
147
|
+
}),
|
|
148
|
+
),
|
|
149
|
+
);
|
|
150
|
+
}),
|
|
151
|
+
Effect.flatMap((exit) => exit),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
export const deserializeRpcHandlers = <T extends RpcHandlers>(
|
|
155
|
+
handlers: SerializedRpcHandlers<T>,
|
|
156
|
+
schema: RpcHandlerDecoders<T>,
|
|
157
|
+
): T =>
|
|
158
|
+
Object.fromEntries(
|
|
159
|
+
Object.entries(schema).map(([key, schema]) => [
|
|
160
|
+
key,
|
|
161
|
+
deserializeRpcHandler(handlers[key as never], schema),
|
|
162
|
+
]),
|
|
163
|
+
) as T;
|
|
164
|
+
|
|
165
|
+
export const defineSchema = <T extends RpcHandlers>(schema: {
|
|
166
|
+
[K in keyof T]: T[K] extends RpcHandler<any, infer Success, infer Error>
|
|
167
|
+
? { success: Schema.Codec<Success, any>; error: Schema.Codec<Error, any> }
|
|
168
|
+
: never;
|
|
169
|
+
}) => schema as RpcHandlerEncoders<T> & RpcHandlerDecoders<T>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as FileSystem from "effect/FileSystem";
|
|
4
|
+
import * as Hash from "effect/Hash";
|
|
5
|
+
import * as Layer from "effect/Layer";
|
|
6
|
+
import * as Path from "effect/Path";
|
|
7
|
+
import * as AlchemyContext from "../AlchemyContext.ts";
|
|
8
|
+
|
|
9
|
+
export class RpcPaths extends Context.Service<
|
|
10
|
+
RpcPaths,
|
|
11
|
+
{ readonly lock: string; readonly url: string }
|
|
12
|
+
>()("RpcPaths") {}
|
|
13
|
+
|
|
14
|
+
export const layer = (main: string) =>
|
|
15
|
+
Layer.effect(
|
|
16
|
+
RpcPaths,
|
|
17
|
+
Effect.gen(function* () {
|
|
18
|
+
const { dotAlchemy } = yield* AlchemyContext.AlchemyContext;
|
|
19
|
+
const path = yield* Path.Path;
|
|
20
|
+
const fs = yield* FileSystem.FileSystem;
|
|
21
|
+
const id = Math.abs(Hash.string(main));
|
|
22
|
+
const dir = path.resolve(dotAlchemy, "local");
|
|
23
|
+
yield* fs.makeDirectory(dir, { recursive: true });
|
|
24
|
+
return RpcPaths.of({
|
|
25
|
+
lock: path.join(dir, `${id}.lock`),
|
|
26
|
+
url: path.join(dir, `${id}.url`),
|
|
27
|
+
});
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Fiber from "effect/Fiber";
|
|
3
|
+
import * as FileSystem from "effect/FileSystem";
|
|
4
|
+
import * as Layer from "effect/Layer";
|
|
5
|
+
import * as Schedule from "effect/Schedule";
|
|
6
|
+
import * as AlchemyContext from "../AlchemyContext.ts";
|
|
7
|
+
import * as Lock from "./Lock.ts";
|
|
8
|
+
import {
|
|
9
|
+
serializeRpcHandlers,
|
|
10
|
+
type RpcHandlerEncoders,
|
|
11
|
+
type RpcHandlers,
|
|
12
|
+
} from "./RpcHandler.ts";
|
|
13
|
+
import * as RpcPaths from "./RpcPaths.ts";
|
|
14
|
+
import { makeBunWebSocketRpcServer } from "./RpcTransport.ts";
|
|
15
|
+
|
|
16
|
+
export const layerServices = (main: string) =>
|
|
17
|
+
Layer.provideMerge(
|
|
18
|
+
Lock.LockLive,
|
|
19
|
+
Layer.provideMerge(RpcPaths.layer(main), AlchemyContext.AlchemyContextLive),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export const makeRpcServer = Effect.fn(function* <T extends RpcHandlers, E, R>(
|
|
23
|
+
handlersEffect: Effect.Effect<T, E, R>,
|
|
24
|
+
schema: RpcHandlerEncoders<T>,
|
|
25
|
+
) {
|
|
26
|
+
const lock = yield* Lock.Lock.use((lock) => lock.acquire);
|
|
27
|
+
const fs = yield* FileSystem.FileSystem;
|
|
28
|
+
const paths = yield* RpcPaths.RpcPaths;
|
|
29
|
+
|
|
30
|
+
const server = yield* Effect.gen(function* () {
|
|
31
|
+
const heartbeat = yield* Heartbeat;
|
|
32
|
+
const handlers = yield* handlersEffect;
|
|
33
|
+
const server = yield* Effect.acquireRelease(
|
|
34
|
+
Effect.sync(() =>
|
|
35
|
+
makeBunWebSocketRpcServer(() => {
|
|
36
|
+
const methods = Object.assign(
|
|
37
|
+
serializeRpcHandlers(handlers, schema),
|
|
38
|
+
{
|
|
39
|
+
heartbeat: () => Effect.runPromise(heartbeat.touch),
|
|
40
|
+
shutdown: () => Effect.runPromise(heartbeat.shutdown),
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
void methods.heartbeat();
|
|
44
|
+
return methods;
|
|
45
|
+
}),
|
|
46
|
+
),
|
|
47
|
+
(server) => Effect.promise(() => server.stop(true)),
|
|
48
|
+
);
|
|
49
|
+
const address = `ws://${server.hostname}:${server.port}`;
|
|
50
|
+
yield* fs.writeFileString(paths.url, address);
|
|
51
|
+
yield* Effect.addFinalizer(() =>
|
|
52
|
+
fs.readFileString(paths.url).pipe(
|
|
53
|
+
Effect.flatMap((text) =>
|
|
54
|
+
text === address ? fs.remove(paths.url) : Effect.void,
|
|
55
|
+
),
|
|
56
|
+
Effect.ignore,
|
|
57
|
+
),
|
|
58
|
+
);
|
|
59
|
+
yield* heartbeat.await;
|
|
60
|
+
}).pipe(Effect.forkScoped);
|
|
61
|
+
|
|
62
|
+
yield* Fiber.joinAll([lock, server]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const Heartbeat = Effect.gen(function* () {
|
|
66
|
+
let last = Date.now();
|
|
67
|
+
const fiber = yield* Effect.suspend(() => {
|
|
68
|
+
if (Date.now() - last > 10_000) {
|
|
69
|
+
return Effect.fail({ _tag: "Timeout" } as const);
|
|
70
|
+
}
|
|
71
|
+
return Effect.void;
|
|
72
|
+
}).pipe(Effect.repeat(Schedule.spaced("4 seconds")), Effect.forkScoped);
|
|
73
|
+
return {
|
|
74
|
+
touch: Effect.sync(() => {
|
|
75
|
+
last = Date.now();
|
|
76
|
+
}),
|
|
77
|
+
shutdown: Fiber.interrupt(fiber),
|
|
78
|
+
await: Fiber.join(fiber),
|
|
79
|
+
};
|
|
80
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ServerWebSocket } from "bun";
|
|
2
|
+
import type { RpcCompatible, RpcTransport } from "capnweb";
|
|
3
|
+
import { RpcSession } from "capnweb";
|
|
4
|
+
|
|
5
|
+
export function makeBunWebSocketRpcServer<T extends RpcCompatible<T>>(
|
|
6
|
+
main: () => T,
|
|
7
|
+
) {
|
|
8
|
+
return Bun.serve<{
|
|
9
|
+
transport: BunWebSocketRpcTransport;
|
|
10
|
+
session: RpcSession<T>;
|
|
11
|
+
}>({
|
|
12
|
+
port: 0,
|
|
13
|
+
fetch: (request, server) => {
|
|
14
|
+
if (server.upgrade(request, { data: undefined! })) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
return new Response("Upgrade failed", { status: 400 });
|
|
18
|
+
},
|
|
19
|
+
websocket: {
|
|
20
|
+
open: (ws) => {
|
|
21
|
+
const transport = new BunWebSocketRpcTransport(ws);
|
|
22
|
+
const session = new RpcSession<T>(transport, main());
|
|
23
|
+
ws.data = { transport, session };
|
|
24
|
+
},
|
|
25
|
+
message: (ws, message) => {
|
|
26
|
+
ws.data.transport.dispatchMessage(message);
|
|
27
|
+
},
|
|
28
|
+
close: (ws, code, reason) => {
|
|
29
|
+
ws.data.transport.dispatchClose(code, reason);
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class BunWebSocketRpcTransport implements RpcTransport {
|
|
36
|
+
private receiveQueue: Array<string> = [];
|
|
37
|
+
private receiveResolver?: (value: string) => void;
|
|
38
|
+
private receiveRejecter?: (reason: unknown) => void;
|
|
39
|
+
private error?: unknown;
|
|
40
|
+
constructor(private readonly ws: ServerWebSocket<any>) {}
|
|
41
|
+
async send(message: string): Promise<void> {
|
|
42
|
+
this.ws.send(message);
|
|
43
|
+
}
|
|
44
|
+
async receive(): Promise<string> {
|
|
45
|
+
const next = this.receiveQueue.shift();
|
|
46
|
+
if (next) {
|
|
47
|
+
return next;
|
|
48
|
+
} else if (this.error) {
|
|
49
|
+
throw this.error;
|
|
50
|
+
}
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
this.receiveResolver = resolve;
|
|
53
|
+
this.receiveRejecter = reject;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
abort?(reason: any): void {
|
|
57
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
58
|
+
this.ws.close(3000, message);
|
|
59
|
+
this.error ??= reason;
|
|
60
|
+
}
|
|
61
|
+
dispatchMessage(data: string | Buffer<ArrayBuffer>): void {
|
|
62
|
+
if (this.error) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
data = typeof data === "string" ? data : data.toString("utf-8");
|
|
66
|
+
|
|
67
|
+
if (this.receiveResolver) {
|
|
68
|
+
this.receiveResolver(data);
|
|
69
|
+
this.receiveResolver = undefined;
|
|
70
|
+
this.receiveRejecter = undefined;
|
|
71
|
+
} else {
|
|
72
|
+
this.receiveQueue.push(data);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
dispatchClose(code: number, reason: string): void {
|
|
76
|
+
if (!this.error) {
|
|
77
|
+
this.error = new Error(`WebSocket closed with code ${code}: ${reason}`);
|
|
78
|
+
if (this.receiveRejecter) {
|
|
79
|
+
this.receiveRejecter(this.error);
|
|
80
|
+
this.receiveRejecter = undefined;
|
|
81
|
+
this.receiveResolver = undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
package/src/Telemetry/Metrics.ts
CHANGED
|
@@ -30,6 +30,24 @@ export const cliCounter = Metric.counter("alchemy.cli.invocations", {
|
|
|
30
30
|
incremental: true,
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Counter for Cloudflare State Store bootstrap/deploy operations.
|
|
35
|
+
* Tagged per call with `op` (e.g. `deploy`) and `status`
|
|
36
|
+
* (`success`/`error`) so that the deploy success rate of the
|
|
37
|
+
* state store itself can be tracked separately from regular
|
|
38
|
+
* resource lifecycle ops.
|
|
39
|
+
*/
|
|
40
|
+
export const stateStoreCounter = Metric.counter(
|
|
41
|
+
"alchemy.state_store.operations",
|
|
42
|
+
{
|
|
43
|
+
description:
|
|
44
|
+
"Number of Cloudflare State Store deploy/bootstrap operations dispatched by alchemy.",
|
|
45
|
+
incremental: true,
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
export type StateStoreOp = "deploy";
|
|
50
|
+
|
|
33
51
|
/**
|
|
34
52
|
* Wraps a resource lifecycle Effect to record a counter + timer entry,
|
|
35
53
|
* tagged with `resource_type`, `op`, and `status` (`success`/`error`).
|
|
@@ -75,6 +93,26 @@ export const recordCli =
|
|
|
75
93
|
),
|
|
76
94
|
);
|
|
77
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Wraps a Cloudflare State Store deploy/bootstrap Effect to bump
|
|
98
|
+
* {@link stateStoreCounter} with the outcome (`success`/`error`),
|
|
99
|
+
* tagged with the given `op`.
|
|
100
|
+
*/
|
|
101
|
+
export const recordStateStoreOp =
|
|
102
|
+
(op: StateStoreOp) =>
|
|
103
|
+
<A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
|
|
104
|
+
self.pipe(
|
|
105
|
+
Effect.onExit((exit) =>
|
|
106
|
+
Metric.update(
|
|
107
|
+
Metric.withAttributes(stateStoreCounter, {
|
|
108
|
+
op,
|
|
109
|
+
status: Exit.isSuccess(exit) ? "success" : "error",
|
|
110
|
+
}),
|
|
111
|
+
1,
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
);
|
|
115
|
+
|
|
78
116
|
export type ResourceOp = "precreate" | "create" | "update" | "delete" | "read";
|
|
79
117
|
|
|
80
118
|
const elapsed = (startNs: bigint): Duration.Duration =>
|
|
@@ -5,6 +5,8 @@ import type { Path } from "effect/Path";
|
|
|
5
5
|
import type { Teardown } from "effect/Runtime";
|
|
6
6
|
import type { Stdio } from "effect/Stdio";
|
|
7
7
|
import type { Terminal } from "effect/Terminal";
|
|
8
|
+
import type { HttpServer } from "effect/unstable/http/HttpServer";
|
|
9
|
+
import type { ServeError } from "effect/unstable/http/HttpServerError";
|
|
8
10
|
import type { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
|
|
9
11
|
import type { WebSocketConstructor } from "effect/unstable/socket/Socket";
|
|
10
12
|
|
|
@@ -60,3 +62,20 @@ export const runMain = <E, A>(
|
|
|
60
62
|
);
|
|
61
63
|
}
|
|
62
64
|
};
|
|
65
|
+
|
|
66
|
+
export const httpServer = (
|
|
67
|
+
port: number = 0,
|
|
68
|
+
host: string = "localhost",
|
|
69
|
+
): Layer.Layer<HttpServer, ServeError> =>
|
|
70
|
+
Effect.promise(async () => {
|
|
71
|
+
if (isBun) {
|
|
72
|
+
const BunHttpServer = await import("@effect/platform-bun/BunHttpServer");
|
|
73
|
+
return BunHttpServer.layer({ hostname: host, port });
|
|
74
|
+
} else {
|
|
75
|
+
const [NodeHttpServer, Http] = await Promise.all([
|
|
76
|
+
import("@effect/platform-node/NodeHttpServer"),
|
|
77
|
+
import("node:http"),
|
|
78
|
+
]);
|
|
79
|
+
return NodeHttpServer.layerServer(Http.createServer, { host, port });
|
|
80
|
+
}
|
|
81
|
+
}).pipe(Layer.unwrap);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type * as Context from "effect/Context";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import type * as Scope from "effect/Scope";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Builds a layer with the current scope.
|
|
8
|
+
* @param layer - The layer to build.
|
|
9
|
+
* @returns A scoped effect returning a Context with the provided layer.
|
|
10
|
+
*/
|
|
11
|
+
export const buildLayerScoped = <ROut, E, RIn>(
|
|
12
|
+
layer: Layer.Layer<ROut, E, RIn>,
|
|
13
|
+
): Effect.Effect<Context.Context<ROut>, E, RIn | Scope.Scope> =>
|
|
14
|
+
Effect.flatMap(Effect.scope, (scope) => Layer.buildWithScope(layer, scope));
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Builds a layer with the current scope and provides it to the given effect.
|
|
18
|
+
* @param layer - The layer to build.
|
|
19
|
+
*/
|
|
20
|
+
export const provideLayerScoped =
|
|
21
|
+
<ROut, E1, RIn>(layer: Layer.Layer<ROut, E1, RIn>) =>
|
|
22
|
+
<A, E2, R>(effect: Effect.Effect<A, E2, R>) =>
|
|
23
|
+
Effect.flatMap(buildLayerScoped(layer), (context) =>
|
|
24
|
+
Effect.provideContext(effect, context),
|
|
25
|
+
);
|