alchemy 2.0.0-beta.43 → 2.0.0-beta.44

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.
Files changed (207) hide show
  1. package/bin/cli.js +18 -5
  2. package/bin/exec.js +315 -233
  3. package/bin/exec.js.map +1 -1
  4. package/lib/Auth/Lock.js +2 -2
  5. package/lib/Auth/Lock.js.map +1 -1
  6. package/lib/Bundle/Bundle.d.ts +11 -0
  7. package/lib/Bundle/Bundle.d.ts.map +1 -1
  8. package/lib/Bundle/Bundle.js +19 -12
  9. package/lib/Bundle/Bundle.js.map +1 -1
  10. package/lib/Bundle/BundleAnalyzerPlugin.d.ts +25 -0
  11. package/lib/Bundle/BundleAnalyzerPlugin.d.ts.map +1 -0
  12. package/lib/Bundle/BundleAnalyzerPlugin.js +16 -0
  13. package/lib/Bundle/BundleAnalyzerPlugin.js.map +1 -0
  14. package/lib/Bundle/RawPlugin.d.ts +36 -0
  15. package/lib/Bundle/RawPlugin.d.ts.map +1 -0
  16. package/lib/Bundle/RawPlugin.js +80 -0
  17. package/lib/Bundle/RawPlugin.js.map +1 -0
  18. package/lib/Bundle/index.d.ts +2 -0
  19. package/lib/Bundle/index.d.ts.map +1 -1
  20. package/lib/Bundle/index.js +2 -0
  21. package/lib/Bundle/index.js.map +1 -1
  22. package/lib/Cli/commands/dev.d.ts +3 -2
  23. package/lib/Cli/commands/dev.d.ts.map +1 -1
  24. package/lib/Cli/commands/dev.js +9 -1
  25. package/lib/Cli/commands/dev.js.map +1 -1
  26. package/lib/Cli/exec.d.ts.map +1 -1
  27. package/lib/Cli/exec.js +2 -1
  28. package/lib/Cli/exec.js.map +1 -1
  29. package/lib/Cloudflare/Local.d.ts +2 -0
  30. package/lib/Cloudflare/Local.d.ts.map +1 -0
  31. package/lib/Cloudflare/Local.js +9 -0
  32. package/lib/Cloudflare/Local.js.map +1 -0
  33. package/lib/Cloudflare/Providers.d.ts.map +1 -1
  34. package/lib/Cloudflare/Providers.js +3 -1
  35. package/lib/Cloudflare/Providers.js.map +1 -1
  36. package/lib/Cloudflare/Workers/DurableObjectBridge.d.ts.map +1 -1
  37. package/lib/Cloudflare/Workers/DurableObjectBridge.js +0 -1
  38. package/lib/Cloudflare/Workers/DurableObjectBridge.js.map +1 -1
  39. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts +22 -3
  40. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -1
  41. package/lib/Cloudflare/Workers/LocalWorkerProvider.js +413 -21
  42. package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -1
  43. package/lib/Cloudflare/Workers/Worker.d.ts +6 -19
  44. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  45. package/lib/Cloudflare/Workers/Worker.js +3 -4
  46. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  47. package/lib/Cloudflare/Workers/WorkerBridge.d.ts.map +1 -1
  48. package/lib/Cloudflare/Workers/WorkerBridge.js +3 -5
  49. package/lib/Cloudflare/Workers/WorkerBridge.js.map +1 -1
  50. package/lib/Cloudflare/Workers/WorkerBundle.d.ts +1 -2
  51. package/lib/Cloudflare/Workers/WorkerBundle.d.ts.map +1 -1
  52. package/lib/Cloudflare/Workers/WorkerBundle.js +3 -6
  53. package/lib/Cloudflare/Workers/WorkerBundle.js.map +1 -1
  54. package/lib/Cloudflare/Zaraz/ZarazConfig.d.ts +184 -0
  55. package/lib/Cloudflare/Zaraz/ZarazConfig.d.ts.map +1 -0
  56. package/lib/Cloudflare/Zaraz/ZarazConfig.js +216 -0
  57. package/lib/Cloudflare/Zaraz/ZarazConfig.js.map +1 -0
  58. package/lib/Cloudflare/Zaraz/ZarazEventTypes.d.ts +188 -0
  59. package/lib/Cloudflare/Zaraz/ZarazEventTypes.d.ts.map +1 -0
  60. package/lib/Cloudflare/Zaraz/ZarazEventTypes.js +4 -0
  61. package/lib/Cloudflare/Zaraz/ZarazEventTypes.js.map +1 -0
  62. package/lib/Cloudflare/Zaraz/index.d.ts +3 -0
  63. package/lib/Cloudflare/Zaraz/index.d.ts.map +1 -0
  64. package/lib/Cloudflare/Zaraz/index.js +2 -0
  65. package/lib/Cloudflare/Zaraz/index.js.map +1 -0
  66. package/lib/Cloudflare/index.d.ts +1 -0
  67. package/lib/Cloudflare/index.d.ts.map +1 -1
  68. package/lib/Cloudflare/index.js +1 -0
  69. package/lib/Cloudflare/index.js.map +1 -1
  70. package/lib/Local/RpcProvider.d.ts +54 -0
  71. package/lib/Local/RpcProvider.d.ts.map +1 -0
  72. package/lib/Local/RpcProvider.js +57 -0
  73. package/lib/Local/RpcProvider.js.map +1 -0
  74. package/lib/Local/RpcProviderProxy.d.ts +19 -0
  75. package/lib/Local/RpcProviderProxy.d.ts.map +1 -0
  76. package/lib/Local/RpcProviderProxy.js +48 -0
  77. package/lib/Local/RpcProviderProxy.js.map +1 -0
  78. package/lib/Local/RpcSerialization.d.ts +34 -0
  79. package/lib/Local/RpcSerialization.d.ts.map +1 -0
  80. package/lib/Local/RpcSerialization.js +120 -0
  81. package/lib/Local/RpcSerialization.js.map +1 -0
  82. package/lib/Local/RpcServer.d.ts +63 -0
  83. package/lib/Local/RpcServer.d.ts.map +1 -0
  84. package/lib/Local/RpcServer.js +72 -0
  85. package/lib/Local/RpcServer.js.map +1 -0
  86. package/lib/Local/RpcServerBun.d.ts +3 -0
  87. package/lib/Local/RpcServerBun.d.ts.map +1 -0
  88. package/lib/Local/RpcServerBun.js +47 -0
  89. package/lib/Local/RpcServerBun.js.map +1 -0
  90. package/lib/Local/RpcServerEnvironment.d.ts +20 -0
  91. package/lib/Local/RpcServerEnvironment.d.ts.map +1 -0
  92. package/lib/Local/RpcServerEnvironment.js +22 -0
  93. package/lib/Local/RpcServerEnvironment.js.map +1 -0
  94. package/lib/Local/RpcServerNode.d.ts +3 -0
  95. package/lib/Local/RpcServerNode.d.ts.map +1 -0
  96. package/lib/Local/RpcServerNode.js +43 -0
  97. package/lib/Local/RpcServerNode.js.map +1 -0
  98. package/lib/Local/RpcServerSession.d.ts +27 -0
  99. package/lib/Local/RpcServerSession.d.ts.map +1 -0
  100. package/lib/Local/RpcServerSession.js +68 -0
  101. package/lib/Local/RpcServerSession.js.map +1 -0
  102. package/lib/Local/RpcSpawner.d.ts +21 -0
  103. package/lib/Local/RpcSpawner.d.ts.map +1 -0
  104. package/lib/Local/RpcSpawner.js +132 -0
  105. package/lib/Local/RpcSpawner.js.map +1 -0
  106. package/lib/Util/PlatformServices.d.ts +7 -0
  107. package/lib/Util/PlatformServices.d.ts.map +1 -1
  108. package/lib/Util/PlatformServices.js +22 -11
  109. package/lib/Util/PlatformServices.js.map +1 -1
  110. package/lib/Util/camel.d.ts.map +1 -1
  111. package/lib/Util/camel.js +2 -3
  112. package/lib/Util/camel.js.map +1 -1
  113. package/lib/Util/data.d.ts +5 -0
  114. package/lib/Util/data.d.ts.map +1 -1
  115. package/lib/Util/data.js +29 -0
  116. package/lib/Util/data.js.map +1 -1
  117. package/lib/Util/stable.d.ts.map +1 -1
  118. package/lib/Util/stable.js +3 -6
  119. package/lib/Util/stable.js.map +1 -1
  120. package/lib/tsconfig.test.tsbuildinfo +1 -1
  121. package/package.json +12 -14
  122. package/src/Auth/Lock.ts +2 -2
  123. package/src/Bundle/Bundle.ts +35 -13
  124. package/src/Bundle/BundleAnalyzerPlugin.ts +33 -0
  125. package/src/Bundle/RawPlugin.ts +82 -0
  126. package/src/Bundle/index.ts +2 -0
  127. package/src/Cli/commands/dev.ts +51 -37
  128. package/src/Cli/exec.ts +12 -7
  129. package/src/Cloudflare/Local.ts +23 -0
  130. package/src/Cloudflare/Providers.ts +3 -0
  131. package/src/Cloudflare/Workers/DurableObjectBridge.ts +0 -1
  132. package/src/Cloudflare/Workers/LocalWorkerProvider.ts +547 -21
  133. package/src/Cloudflare/Workers/Worker.ts +11 -22
  134. package/src/Cloudflare/Workers/WorkerBridge.ts +3 -5
  135. package/src/Cloudflare/Workers/WorkerBundle.ts +4 -8
  136. package/src/Cloudflare/Zaraz/ZarazConfig.ts +403 -0
  137. package/src/Cloudflare/Zaraz/ZarazEventTypes.ts +257 -0
  138. package/src/Cloudflare/Zaraz/index.ts +25 -0
  139. package/src/Cloudflare/index.ts +1 -0
  140. package/src/Local/RpcProvider.ts +102 -0
  141. package/src/Local/RpcProviderProxy.ts +86 -0
  142. package/src/Local/RpcSerialization.ts +225 -0
  143. package/src/Local/RpcServer.ts +138 -0
  144. package/src/Local/RpcServerBun.ts +59 -0
  145. package/src/Local/RpcServerEnvironment.ts +55 -0
  146. package/src/Local/RpcServerNode.ts +58 -0
  147. package/src/Local/RpcServerSession.ts +87 -0
  148. package/src/Local/RpcSpawner.ts +196 -0
  149. package/src/Util/PlatformServices.ts +41 -27
  150. package/src/Util/camel.ts +3 -5
  151. package/src/Util/data.ts +38 -0
  152. package/src/Util/stable.ts +4 -10
  153. package/lib/Cloudflare/Local/Sidecar.d.ts +0 -63
  154. package/lib/Cloudflare/Local/Sidecar.d.ts.map +0 -1
  155. package/lib/Cloudflare/Local/Sidecar.js +0 -50
  156. package/lib/Cloudflare/Local/Sidecar.js.map +0 -1
  157. package/lib/Cloudflare/Local/SidecarHandlers.d.ts +0 -8
  158. package/lib/Cloudflare/Local/SidecarHandlers.d.ts.map +0 -1
  159. package/lib/Cloudflare/Local/SidecarHandlers.js +0 -381
  160. package/lib/Cloudflare/Local/SidecarHandlers.js.map +0 -1
  161. package/lib/Cloudflare/Local/SidecarServer.d.ts +0 -2
  162. package/lib/Cloudflare/Local/SidecarServer.d.ts.map +0 -1
  163. package/lib/Cloudflare/Local/SidecarServer.js +0 -34
  164. package/lib/Cloudflare/Local/SidecarServer.js.map +0 -1
  165. package/lib/Sidecar/Lock.d.ts +0 -23
  166. package/lib/Sidecar/Lock.d.ts.map +0 -1
  167. package/lib/Sidecar/Lock.js +0 -39
  168. package/lib/Sidecar/Lock.js.map +0 -1
  169. package/lib/Sidecar/RpcClient.d.ts +0 -15
  170. package/lib/Sidecar/RpcClient.d.ts.map +0 -1
  171. package/lib/Sidecar/RpcClient.js +0 -78
  172. package/lib/Sidecar/RpcClient.js.map +0 -1
  173. package/lib/Sidecar/RpcHandler.d.ts +0 -51
  174. package/lib/Sidecar/RpcHandler.d.ts.map +0 -1
  175. package/lib/Sidecar/RpcHandler.js +0 -87
  176. package/lib/Sidecar/RpcHandler.js.map +0 -1
  177. package/lib/Sidecar/RpcPaths.d.ts +0 -19
  178. package/lib/Sidecar/RpcPaths.d.ts.map +0 -1
  179. package/lib/Sidecar/RpcPaths.js +0 -31
  180. package/lib/Sidecar/RpcPaths.js.map +0 -1
  181. package/lib/Sidecar/RpcServer.d.ts +0 -30
  182. package/lib/Sidecar/RpcServer.d.ts.map +0 -1
  183. package/lib/Sidecar/RpcServer.js +0 -105
  184. package/lib/Sidecar/RpcServer.js.map +0 -1
  185. package/lib/Sidecar/RpcServerBun.d.ts +0 -4
  186. package/lib/Sidecar/RpcServerBun.d.ts.map +0 -1
  187. package/lib/Sidecar/RpcServerBun.js +0 -28
  188. package/lib/Sidecar/RpcServerBun.js.map +0 -1
  189. package/lib/Sidecar/RpcServerNode.d.ts +0 -4
  190. package/lib/Sidecar/RpcServerNode.d.ts.map +0 -1
  191. package/lib/Sidecar/RpcServerNode.js +0 -39
  192. package/lib/Sidecar/RpcServerNode.js.map +0 -1
  193. package/lib/Sidecar/RpcServices.d.ts +0 -6
  194. package/lib/Sidecar/RpcServices.d.ts.map +0 -1
  195. package/lib/Sidecar/RpcServices.js +0 -29
  196. package/lib/Sidecar/RpcServices.js.map +0 -1
  197. package/src/Cloudflare/Local/Sidecar.ts +0 -81
  198. package/src/Cloudflare/Local/SidecarHandlers.ts +0 -491
  199. package/src/Cloudflare/Local/SidecarServer.ts +0 -70
  200. package/src/Sidecar/Lock.ts +0 -66
  201. package/src/Sidecar/RpcClient.ts +0 -142
  202. package/src/Sidecar/RpcHandler.ts +0 -205
  203. package/src/Sidecar/RpcPaths.ts +0 -39
  204. package/src/Sidecar/RpcServer.ts +0 -153
  205. package/src/Sidecar/RpcServerBun.ts +0 -41
  206. package/src/Sidecar/RpcServerNode.ts +0 -51
  207. package/src/Sidecar/RpcServices.ts +0 -48
@@ -0,0 +1,25 @@
1
+ export * from "./ZarazConfig.ts";
2
+ export type {
3
+ InferZarazEcommerceEvents,
4
+ InferZarazEvents,
5
+ ZarazClientProperties,
6
+ ZarazEcommerce,
7
+ ZarazEcommerceCheckoutStep,
8
+ ZarazEcommerceEvents,
9
+ ZarazEcommerceOrder,
10
+ ZarazEcommercePayment,
11
+ ZarazEcommerceProduct,
12
+ ZarazEcommercePromotion,
13
+ ZarazEcommerceRefund,
14
+ ZarazEcommerceSearch,
15
+ ZarazEventContract,
16
+ ZarazEventMap,
17
+ ZarazEventName,
18
+ ZarazEventProperties,
19
+ ZarazHttpEvent,
20
+ ZarazHttpEventsPayload,
21
+ ZarazSetScope,
22
+ ZarazSystem,
23
+ ZarazTrack,
24
+ ZarazWebApi,
25
+ } from "./ZarazEventTypes.ts";
@@ -21,3 +21,4 @@ export * from "./VpcService/index.ts";
21
21
  export * from "./Website/index.ts";
22
22
  export * from "./Workers/index.ts";
23
23
  export * from "./Workers/RpcAsync.ts";
24
+ export * from "./Zaraz/index.ts";
@@ -0,0 +1,102 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Layer from "effect/Layer";
3
+ import type { Scope } from "effect/Scope";
4
+ import type { Platform } from "../Platform.ts";
5
+ import * as Provider from "../Provider.ts";
6
+ import type { ResourceClass, ResourceLike } from "../Resource.ts";
7
+ import { RpcProviderProxy } from "./RpcProviderProxy.ts";
8
+
9
+ /**
10
+ * Constructs an RpcProvider for a given resource.
11
+ *
12
+ * An RpcProvider is a resource provider that can run in a separate process.
13
+ * This is used for local development so that the resource provider and its state can outlive the user's code when it is hot-reloaded.
14
+ * If the {@link RpcProviderProxy} is provided, the provider is run in a separate process, which we communicate with here using a websocket.
15
+ * Otherwise, the given effect is used to construct the provider directly.
16
+ *
17
+ * @note If the provider is running in a separate process, you may not need to construct all of its layers in this process.
18
+ * You can use {@link providerServices} or {@link providerServicesEffect} to construct such layers only when they are needed.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * export const MyProvider = RpcProvider.effect(
23
+ * MyResource,
24
+ * import.meta.resolve("./my-rpc-provider-server.ts"),
25
+ * Effect.gen(function* () {
26
+ * return {
27
+ * diff: ...,
28
+ * reconcile: ...,
29
+ * delete: ...,
30
+ * }
31
+ * })
32
+ * )
33
+ * ```
34
+ *
35
+ * @param cls - The tag of the resource class to construct a provider for.
36
+ * @param serverEntryUrl - The main file for the server entry point, if the provider is to be run in a separate process. This is typically obtained using `import.meta.url` or `import.meta.resolve`.
37
+ * @param eff - The Effect to use to construct the provider.
38
+ * @returns A layer containing the RpcProvider.
39
+ */
40
+ export const effect = <
41
+ R extends ResourceLike,
42
+ Req = never,
43
+ ReadReq = never,
44
+ DiffReq = never,
45
+ PrecreateReq = never,
46
+ ReconcileReq = never,
47
+ DeleteReq = never,
48
+ TailReq = never,
49
+ LogsReq = never,
50
+ >(
51
+ cls: ResourceClass<R> | Platform<R, any, any, any, any>,
52
+ serverEntryUrl: string,
53
+ eff: Effect.Effect<
54
+ Provider.ProviderService<
55
+ R,
56
+ ReadReq,
57
+ DiffReq,
58
+ PrecreateReq,
59
+ ReconcileReq,
60
+ DeleteReq,
61
+ TailReq,
62
+ LogsReq
63
+ >,
64
+ never,
65
+ Req
66
+ >,
67
+ ) =>
68
+ Provider.effect(
69
+ cls,
70
+ Effect.gen(function* () {
71
+ const client = yield* Effect.serviceOption(RpcProviderProxy);
72
+ if (client._tag === "None") return yield* eff;
73
+ return yield* client.value.get(serverEntryUrl, cls.Type);
74
+ }),
75
+ );
76
+
77
+ /**
78
+ * Conditionally constructs a layer for use by an RpcProvider.
79
+ * If the {@link RpcProviderProxy} is present in context, the layer is empty because it will not be used in this process.
80
+ * Otherwise, the given layer is returned.
81
+ * @param self - The layer that is used by the RpcProvider.
82
+ * @returns A layer that is empty if the {@link RpcProviderProxy} is present in context, otherwise the given layer.
83
+ */
84
+ export const providerServices = <ROut, E, RIn>(
85
+ self: Layer.Layer<ROut, E, RIn>,
86
+ ): Layer.Layer<ROut, E, RIn> => providerServicesEffect(Effect.succeed(self));
87
+
88
+ /**
89
+ * Conditionally constructs a layer for use by an RpcProvider.
90
+ * If the {@link RpcProviderProxy} is present in context, the layer is empty because it will not be used in this process.
91
+ * Otherwise, the given layer is returned.
92
+ * @param self - An effect which returns a layer that is used by the RpcProvider.
93
+ */
94
+ export const providerServicesEffect = <A, E1, R1, E, R>(
95
+ self: Effect.Effect<Layer.Layer<A, E1, R1>, E, R>,
96
+ ): Layer.Layer<A, E | E1, R1 | Exclude<R, Scope>> =>
97
+ Effect.serviceOption(RpcProviderProxy).pipe(
98
+ Effect.flatMap((client) =>
99
+ client._tag === "None" ? self : (Effect.succeed(Layer.empty) as never),
100
+ ),
101
+ Layer.unwrap,
102
+ );
@@ -0,0 +1,86 @@
1
+ import { newWebSocketRpcSession, type RpcStub } from "capnweb";
2
+ import * as Cache from "effect/Cache";
3
+ import * as Config from "effect/Config";
4
+ import * as Context from "effect/Context";
5
+ import * as Effect from "effect/Effect";
6
+ import * as Layer from "effect/Layer";
7
+ import * as HttpBody from "effect/unstable/http/HttpBody";
8
+ import * as HttpClient from "effect/unstable/http/HttpClient";
9
+ import { AlchemyContext } from "../AlchemyContext.ts";
10
+ import type { ProviderService } from "../Provider.ts";
11
+ import type { ResourceLike } from "../Resource.ts";
12
+ import { Stack } from "../Stack.ts";
13
+ import { unwrapRpcHandlers } from "./RpcSerialization.ts";
14
+ import type { RpcProxyApi } from "./RpcServer.ts";
15
+ import type { RpcSpawnPayload } from "./RpcSpawner.ts";
16
+
17
+ export class RpcProviderProxy extends Context.Service<
18
+ RpcProviderProxy,
19
+ {
20
+ readonly get: <R extends ResourceLike>(
21
+ serverEntryUrl: string,
22
+ providerName: R["Type"],
23
+ ) => Effect.Effect<ProviderService<R>, never, AlchemyContext | Stack>;
24
+ }
25
+ >()("alchemy/Local/RpcProviderProxy") {}
26
+
27
+ export const SPAWNER_URL_ENV_KEY = "ALCHEMY_RPC_SPAWNER_URL" as const;
28
+
29
+ const make = Effect.fnUntraced(function* (spawnerUrl: string) {
30
+ const client = yield* HttpClient.HttpClient;
31
+
32
+ const getSession = Effect.fnUntraced(
33
+ function* (serverEntryUrl: string) {
34
+ const alchemyContext = yield* AlchemyContext;
35
+ const stack = yield* Stack;
36
+ const payload: RpcSpawnPayload = {
37
+ serverEntryUrl,
38
+ alchemyContext,
39
+ stack: { name: stack.name, stage: stack.stage },
40
+ };
41
+ const response = yield* client.post(spawnerUrl, {
42
+ body: yield* HttpBody.json(payload),
43
+ });
44
+ const websocketUrl = yield* response.text;
45
+ return newWebSocketRpcSession(websocketUrl) as RpcStub<RpcProxyApi>;
46
+ },
47
+ (effect, serverEntryUrl) =>
48
+ Effect.catch(effect, (error) =>
49
+ Effect.die(
50
+ new Error(
51
+ `Failed to create provider RPC session for "${serverEntryUrl}"`,
52
+ {
53
+ cause: error,
54
+ },
55
+ ),
56
+ ),
57
+ ),
58
+ );
59
+
60
+ const cache = yield* Cache.make({
61
+ lookup: getSession,
62
+ capacity: Infinity,
63
+ requireServicesAt: "lookup",
64
+ });
65
+
66
+ return RpcProviderProxy.of({
67
+ get: Effect.fnUntraced(function* (mainUrl, providerName) {
68
+ const session = yield* cache.lookup(mainUrl);
69
+ const provider = yield* Effect.promise(
70
+ () =>
71
+ session.getProvider(providerName) as ReturnType<
72
+ RpcProxyApi["getProvider"]
73
+ >,
74
+ );
75
+ return unwrapRpcHandlers(provider, ["tail"]);
76
+ }),
77
+ });
78
+ });
79
+
80
+ export const layer = (url: string) => Layer.effect(RpcProviderProxy, make(url));
81
+
82
+ export const fromEnv = () =>
83
+ Layer.effect(
84
+ RpcProviderProxy,
85
+ Config.string(SPAWNER_URL_ENV_KEY).pipe(Effect.flatMap(make)),
86
+ );
@@ -0,0 +1,225 @@
1
+ import * as Cause from "effect/Cause";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Exit from "effect/Exit";
4
+ import { flow } from "effect/Function";
5
+ import * as Redacted from "effect/Redacted";
6
+ import * as Schema from "effect/Schema";
7
+ import * as Stream from "effect/Stream";
8
+ import * as NodeUtil from "node:util";
9
+ import * as Output from "../Output.ts";
10
+
11
+ type RpcEffectHandler<Args extends Array<any>, Success, Error> = (
12
+ ...args: Args
13
+ ) => Effect.Effect<Success, Error>;
14
+
15
+ type RpcWrappedEffectHandler<Args extends Array<any>, Success, Error> = (
16
+ args: Args,
17
+ ) => Promise<RpcSerializedExit<Success, Error>>;
18
+
19
+ type RpcStreamHandler<Args extends Array<any>, Success, Error> = (
20
+ ...args: Args
21
+ ) => Stream.Stream<Success, Error>;
22
+
23
+ type RpcWrappedStreamHandler<Args extends Array<any>, Success, Error> = (
24
+ args: Args,
25
+ ) => RpcSerializedStream<Success, Error>;
26
+
27
+ type RpcSerializedStream<Success, _Error> = ReadableStream<Success>;
28
+
29
+ type RpcSerializedExit<Success, Error> =
30
+ | { _tag: "Success"; value: Success }
31
+ | { _tag: "Failure"; cause: Array<RpcSerializedCause<Error>> };
32
+
33
+ type RpcSerializedCause<Error> =
34
+ | { _tag: "Fail"; error: Error }
35
+ | { _tag: "Die"; defect: unknown }
36
+ | { _tag: "Interrupt"; fiberId: number | undefined };
37
+
38
+ export type RpcWrapped<T> =
39
+ T extends RpcEffectHandler<infer Args, infer Success, infer Error>
40
+ ? RpcWrappedEffectHandler<Args, Success, Error>
41
+ : T extends RpcStreamHandler<infer Args, infer Success, infer Error>
42
+ ? RpcWrappedStreamHandler<Args, Success, Error>
43
+ : T extends Record<string, any>
44
+ ? { [K in keyof T]: RpcWrapped<T[K]> }
45
+ : T;
46
+
47
+ export type RpcUnwrapped<T> =
48
+ T extends RpcWrappedEffectHandler<infer Args, infer Success, infer Error>
49
+ ? RpcEffectHandler<Args, Success, Error>
50
+ : T extends RpcWrappedStreamHandler<infer Args, infer Success, infer Error>
51
+ ? RpcStreamHandler<Args, Success, Error>
52
+ : T extends Record<string, any>
53
+ ? { [K in keyof T]: RpcUnwrapped<T[K]> }
54
+ : T;
55
+
56
+ export const wrapRpcHandlers = <T extends Record<string, any>>(
57
+ handlers: T,
58
+ streamKeys?: Array<keyof T>,
59
+ ): RpcWrapped<T> => {
60
+ return Object.fromEntries(
61
+ Object.entries(handlers).map(([key, value]) => [
62
+ key,
63
+ typeof value === "function"
64
+ ? streamKeys?.includes(key)
65
+ ? wrapRpcStreamHandler(value)
66
+ : wrapRpcEffectHandler(value)
67
+ : typeof value === "object" && value !== null
68
+ ? wrapRpcHandlers(value)
69
+ : value,
70
+ ]),
71
+ ) as RpcWrapped<T>;
72
+ };
73
+
74
+ export const unwrapRpcHandlers = <T extends Record<string, any>>(
75
+ handlers: RpcWrapped<T>,
76
+ streamKeys?: Array<keyof T>,
77
+ ): RpcUnwrapped<T> => {
78
+ return Object.fromEntries(
79
+ Object.entries(handlers).map(([key, value]) => [
80
+ key,
81
+ typeof value === "function"
82
+ ? streamKeys?.includes(key)
83
+ ? unwrapRpcStreamHandler(value)
84
+ : unwrapRpcEffectHandler(value)
85
+ : typeof value === "object" && value !== null
86
+ ? unwrapRpcHandlers(value)
87
+ : value,
88
+ ]),
89
+ ) as RpcUnwrapped<T>;
90
+ };
91
+
92
+ const serializeError = Schema.encodeSync(Schema.Defect);
93
+
94
+ const wrapRpcEffectHandler = <Args extends Array<any>, Success, Error>(
95
+ handler: RpcEffectHandler<Args, Success, Error>,
96
+ ): RpcWrappedEffectHandler<Args, Success, Error> =>
97
+ flow(
98
+ (args) => deserializeRpcArgs(args) as Args,
99
+ (args) => handler(...args),
100
+ Effect.exit,
101
+ Effect.map((exit): RpcSerializedExit<Success, Error> => {
102
+ if (exit._tag === "Success") {
103
+ return { _tag: "Success", value: exit.value };
104
+ }
105
+ return {
106
+ _tag: "Failure",
107
+ cause: exit.cause.reasons.map((reason): RpcSerializedCause<Error> => {
108
+ switch (reason._tag) {
109
+ case "Fail":
110
+ return { _tag: "Fail", error: serializeError(reason.error) };
111
+ case "Die":
112
+ return { _tag: "Die", defect: serializeError(reason.defect) };
113
+ case "Interrupt":
114
+ return { _tag: "Interrupt", fiberId: reason.fiberId };
115
+ }
116
+ }),
117
+ };
118
+ }),
119
+ Effect.runPromise,
120
+ );
121
+
122
+ const wrapRpcStreamHandler = <Args extends Array<any>, Success, Error>(
123
+ handler: RpcStreamHandler<Args, Success, Error>,
124
+ ): RpcWrappedStreamHandler<Args, Success, Error> =>
125
+ flow(
126
+ (args) => deserializeRpcArgs(args) as Args,
127
+ (args) => handler(...args),
128
+ (stream) => Stream.toReadableStream(stream),
129
+ );
130
+
131
+ const unwrapRpcEffectHandler = <Args extends Array<any>, Success, Error>(
132
+ handler: RpcWrappedEffectHandler<Args, Success, Error>,
133
+ ): RpcEffectHandler<Args, Success, Error> =>
134
+ flow(
135
+ (...args) => serializeRpcArgs(args) as Args,
136
+ (args) => Effect.promise(() => handler(args)),
137
+ Effect.flatMap((exit): Exit.Exit<Success, Error> => {
138
+ if (exit._tag === "Success") {
139
+ return Exit.succeed(exit.value);
140
+ }
141
+ return Exit.failCause(
142
+ Cause.fromReasons(
143
+ exit.cause.map((reason): Cause.Reason<Error> => {
144
+ switch (reason._tag) {
145
+ case "Fail":
146
+ return Cause.makeFailReason(reason.error);
147
+ case "Die":
148
+ return Cause.makeDieReason(reason.defect);
149
+ case "Interrupt":
150
+ return Cause.makeInterruptReason(reason.fiberId);
151
+ }
152
+ }),
153
+ ),
154
+ );
155
+ }),
156
+ );
157
+
158
+ const unwrapRpcStreamHandler = <Args extends Array<any>, Success, Error>(
159
+ handler: RpcWrappedStreamHandler<Args, Success, Error>,
160
+ ): RpcStreamHandler<Args, Success, Error> =>
161
+ flow(
162
+ (...args) => serializeRpcArgs(args) as Args,
163
+ (args) => handler(args),
164
+ (stream) =>
165
+ Stream.fromReadableStream({
166
+ evaluate: () => stream,
167
+ onError: (error) => error as Error,
168
+ }),
169
+ );
170
+
171
+ const serializeRpcArgs = (value: unknown): unknown => {
172
+ if (Redacted.isRedacted(value)) {
173
+ return { _tag: "Redacted", value: Redacted.value(value) };
174
+ }
175
+ if (Output.isOutput(value)) {
176
+ return {
177
+ _tag: "Output",
178
+ description: NodeUtil.inspect(value),
179
+ };
180
+ }
181
+ if (typeof value === "function") {
182
+ return null;
183
+ }
184
+ if (Array.isArray(value)) {
185
+ return value.map(serializeRpcArgs);
186
+ }
187
+ if (value && typeof value === "object" && !("toJSON" in value)) {
188
+ return Object.fromEntries(
189
+ Object.entries(value).map(([key, child]) => [
190
+ key,
191
+ serializeRpcArgs(child),
192
+ ]),
193
+ );
194
+ }
195
+ return value;
196
+ };
197
+
198
+ const deserializeRpcArgs = (value: unknown): unknown => {
199
+ if (Array.isArray(value)) {
200
+ return value.map(deserializeRpcArgs);
201
+ } else if (typeof value === "object" && value !== null) {
202
+ // These values are serialized as `{_tag: "Redacted", value: ...}` and `{_tag: "Output", description: ...}`,
203
+ // so we need to detect them manually - Redacted.isRedacted and Output.isOutput do not work.
204
+ if ("_tag" in value && value._tag === "Redacted" && "value" in value) {
205
+ return Redacted.make(value.value);
206
+ } else if (
207
+ "_tag" in value &&
208
+ value._tag === "Output" &&
209
+ "description" in value &&
210
+ typeof value.description === "string"
211
+ ) {
212
+ return new Output.NamedExpr(
213
+ new Output.EffectExpr(Output.VoidExpr, () => Effect.never),
214
+ value.description,
215
+ );
216
+ }
217
+ return Object.fromEntries(
218
+ Object.entries(value).map(([key, child]) => [
219
+ key,
220
+ deserializeRpcArgs(child),
221
+ ]),
222
+ );
223
+ }
224
+ return value;
225
+ };
@@ -0,0 +1,138 @@
1
+ import * as Console from "effect/Console";
2
+ import * as Context from "effect/Context";
3
+ import * as Deferred from "effect/Deferred";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Layer from "effect/Layer";
6
+ import type * as Scope from "effect/Scope";
7
+ import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
8
+ import type { HttpClient } from "effect/unstable/http/HttpClient";
9
+ import type { ProviderService } from "../Provider.ts";
10
+ import type { ResourceLike } from "../Resource.ts";
11
+ import {
12
+ platformLayer,
13
+ PlatformServices,
14
+ runMain,
15
+ } from "../Util/PlatformServices.ts";
16
+ import * as RpcSerialization from "./RpcSerialization.ts";
17
+ import * as RpcServerEnvironment from "./RpcServerEnvironment.ts";
18
+ import {
19
+ makeServerRpcSession,
20
+ type ServerRpcSession,
21
+ type ServerWebSocketLike,
22
+ } from "./RpcServerSession.ts";
23
+
24
+ /**
25
+ * A service that exposes one or more resource providers over RPC.
26
+ * This returns `never` because it is meant to be used with `Layer.launch` (see {@link launch}).
27
+ */
28
+ export class RpcServer extends Context.Service<RpcServer, never>()(
29
+ "alchemy/Local/RpcServer",
30
+ ) {}
31
+
32
+ /**
33
+ * The RPC API that is implemented by the server and consumed by {@link RpcProviderProxy}.
34
+ */
35
+ export interface RpcProxyApi {
36
+ /**
37
+ * Retrieves a provider from the RPC server context.
38
+ * The consumer must unwrap the provider using {@link RpcSerialization.unwrapRpcHandlers} before using it.
39
+ */
40
+ readonly getProvider: <R extends ResourceLike>(
41
+ type: R["Type"],
42
+ ) => Promise<RpcSerialization.RpcWrapped<ProviderService<R>>>;
43
+ }
44
+
45
+ const serverPlatformLayer = platformLayer({
46
+ bun: async () => {
47
+ const { RpcServerBun } = await import("./RpcServerBun.ts");
48
+ return RpcServerBun;
49
+ },
50
+ node: async () => {
51
+ const { RpcServerNode } = await import("./RpcServerNode.ts");
52
+ return RpcServerNode;
53
+ },
54
+ });
55
+
56
+ /**
57
+ * Launches an RPC server that serves the given providers.
58
+ * Alchemy globals such as `AlchemyContext`, `Profile`, and `Stack` are inherited from the parent via {@link RpcServerEnvironment.fromEnv} and should not be provided manually.
59
+ * `PlatformServices` and `HttpClient` are also included.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * RpcServer.launch(
64
+ * Layer.merge(
65
+ * FunctionProvider,
66
+ * QueueProvider,
67
+ * ),
68
+ * );
69
+ * ```
70
+ *
71
+ * @param providers - A layer containing the providers to serve.
72
+ */
73
+ export const launch = <ROut, E>(
74
+ providers: Layer.Layer<
75
+ ROut,
76
+ E,
77
+ | Scope.Scope
78
+ | RpcServerEnvironment.RpcEnvironmentServices
79
+ | PlatformServices
80
+ | HttpClient
81
+ >,
82
+ ) =>
83
+ serverPlatformLayer.pipe(
84
+ Layer.provide(providers),
85
+ Layer.provide(RpcServerEnvironment.fromEnv()),
86
+ Layer.provide(Layer.merge(PlatformServices, FetchHttpClient.layer)),
87
+ Layer.launch,
88
+ Effect.scoped,
89
+ runMain,
90
+ );
91
+
92
+ /**
93
+ * Constructs an `RpcServer` layer using the given server implementation.
94
+ * @param serve - A function that spawns a websocket server and returns its URL.
95
+ * @returns An `RpcServer` layer.
96
+ */
97
+ export const layerServer = (
98
+ serve: (handlers: {
99
+ /** Creates a new RPC session over the given websocket. */
100
+ createRpcSession: (
101
+ ws: ServerWebSocketLike,
102
+ ) => ServerRpcSession<RpcProxyApi>;
103
+ /** Called when the parent connection, indicated by the `/parent` path, is established. */
104
+ parentConnected: () => void;
105
+ /** Called when the parent disconnects. The server will shut down when this is called. */
106
+ parentDisconnected: () => void;
107
+ }) => Effect.Effect<{ readonly url: string }, never, Scope.Scope>,
108
+ ) =>
109
+ Layer.effect(
110
+ RpcServer,
111
+ Effect.gen(function* () {
112
+ const context = yield* Effect.context();
113
+ const connected = yield* Deferred.make<void>();
114
+ const disconnected = yield* Deferred.make<void>();
115
+ const { url } = yield* serve({
116
+ createRpcSession: (ws) =>
117
+ makeServerRpcSession<RpcProxyApi>(ws, {
118
+ getProvider: async <R extends ResourceLike>(type: R["Type"]) => {
119
+ const provider = context.mapUnsafe.get(type);
120
+ if (!provider) {
121
+ throw new Error(`Provider "${type}" not found`);
122
+ }
123
+ return RpcSerialization.wrapRpcHandlers(
124
+ provider as ProviderService<R>,
125
+ ["tail"],
126
+ );
127
+ },
128
+ }),
129
+ parentConnected: () => Deferred.doneUnsafe(connected, Effect.void),
130
+ parentDisconnected: () =>
131
+ Deferred.doneUnsafe(disconnected, Effect.void),
132
+ });
133
+ yield* Console.log(`<ALCHEMY_RPC_ADDRESS>${url}</ALCHEMY_RPC_ADDRESS>`);
134
+ yield* Deferred.await(connected).pipe(Effect.timeout("10 seconds")); // TODO(john): should the timeout be shorter?
135
+ yield* Deferred.await(disconnected);
136
+ return yield* Effect.interrupt;
137
+ }),
138
+ );
@@ -0,0 +1,59 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as RpcServer from "./RpcServer.ts";
3
+ import type { ServerRpcSession } from "./RpcServerSession.ts";
4
+
5
+ export const RpcServerBun = RpcServer.layerServer(
6
+ Effect.fnUntraced(function* ({
7
+ parentConnected,
8
+ parentDisconnected,
9
+ createRpcSession,
10
+ }) {
11
+ const server = yield* Effect.sync(() =>
12
+ Bun.serve<
13
+ { type: "session"; session: ServerRpcSession<any> } | { type: "parent" }
14
+ >({
15
+ port: 0,
16
+ fetch: (request, server) => {
17
+ const url = new URL(request.url);
18
+ if (
19
+ server.upgrade(request, {
20
+ data:
21
+ url.pathname === "/parent" ? { type: "parent" } : undefined!,
22
+ })
23
+ ) {
24
+ return;
25
+ }
26
+ return new Response("Upgrade failed", { status: 400 });
27
+ },
28
+ websocket: {
29
+ open: (ws) => {
30
+ if (ws.data && ws.data.type === "parent") {
31
+ parentConnected();
32
+ } else {
33
+ ws.data = {
34
+ type: "session",
35
+ session: createRpcSession(ws),
36
+ };
37
+ }
38
+ },
39
+ message: (ws, message) => {
40
+ if (ws.data.type === "session") {
41
+ ws.data.session.dispatch.message(message);
42
+ }
43
+ },
44
+ close: (ws, code, reason) => {
45
+ if (ws.data.type === "session") {
46
+ ws.data.session.dispatch.close(code, reason);
47
+ } else if (ws.data.type === "parent") {
48
+ parentDisconnected();
49
+ }
50
+ },
51
+ },
52
+ }),
53
+ );
54
+ yield* Effect.addFinalizer(() => Effect.sync(() => server.stop(true)));
55
+ return {
56
+ url: `ws://${server.hostname}:${server.port}`,
57
+ };
58
+ }),
59
+ );
@@ -0,0 +1,55 @@
1
+ import * as Config from "effect/Config";
2
+ import * as ConfigProvider from "effect/ConfigProvider";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Layer from "effect/Layer";
5
+ import * as Option from "effect/Option";
6
+ import { AlchemyContext } from "../AlchemyContext.ts";
7
+ import { AuthProviders } from "../Auth/AuthProvider.ts";
8
+ import { CredentialsStoreLive } from "../Auth/Credentials.ts";
9
+ import { ProfileLive, withProfileOverride } from "../Auth/Profile.ts";
10
+ import { Stack } from "../Stack.ts";
11
+ import { Stage } from "../Stage.ts";
12
+ import { loadConfigProvider } from "../Util/ConfigProvider.ts";
13
+
14
+ export interface RpcServerEnvironment {
15
+ alchemyContext: AlchemyContext["Service"];
16
+ profile: string | undefined;
17
+ envFile: string | undefined;
18
+ stack: {
19
+ name: string;
20
+ stage: string;
21
+ };
22
+ }
23
+
24
+ export type RpcEnvironmentServices = Layer.Success<ReturnType<typeof layer>>;
25
+
26
+ export const layer = (environment: RpcServerEnvironment) =>
27
+ Layer.mergeAll(
28
+ ProfileLive,
29
+ CredentialsStoreLive,
30
+ Layer.succeed(AuthProviders, {}),
31
+ ConfigProvider.layer(
32
+ loadConfigProvider(Option.fromUndefinedOr(environment.envFile)).pipe(
33
+ Effect.map((base) => withProfileOverride(base, environment.profile)),
34
+ ),
35
+ ),
36
+ Layer.succeed(AlchemyContext, environment.alchemyContext),
37
+ Layer.succeed(Stack, {
38
+ name: environment.stack.name,
39
+ stage: environment.stack.stage,
40
+ resources: {},
41
+ bindings: {},
42
+ actions: {},
43
+ }),
44
+ Layer.succeed(Stage, environment.stack.stage),
45
+ );
46
+
47
+ export const RPC_SERVER_ENVIRONMENT_KEY =
48
+ "ALCHEMY_RPC_SERVER_ENVIRONMENT" as const;
49
+
50
+ export const fromEnv = () =>
51
+ Config.string(RPC_SERVER_ENVIRONMENT_KEY).pipe(
52
+ Config.map(JSON.parse),
53
+ Effect.map((environment) => layer(environment)),
54
+ Layer.unwrap,
55
+ );